Merge "Allow mDNS on IFF_POINTOPOINT interfaces" into main
diff --git a/bpf/tests/mts/bpf_existence_test.cpp b/bpf/tests/mts/bpf_existence_test.cpp
index 2cfa546..0ecda3d 100644
--- a/bpf/tests/mts/bpf_existence_test.cpp
+++ b/bpf/tests/mts/bpf_existence_test.cpp
@@ -21,7 +21,6 @@
#include <string>
#include <android-base/properties.h>
-#include <android-modules-utils/sdk_level.h>
#include <android/api-level.h>
#include <bpf/BpfUtils.h>
@@ -32,11 +31,6 @@
using std::string;
using android::bpf::isAtLeastKernelVersion;
-using android::modules::sdklevel::IsAtLeastR;
-using android::modules::sdklevel::IsAtLeastS;
-using android::modules::sdklevel::IsAtLeastT;
-using android::modules::sdklevel::IsAtLeastU;
-using android::modules::sdklevel::IsAtLeastV;
#define PLATFORM "/sys/fs/bpf/"
#define TETHERING "/sys/fs/bpf/tethering/"
@@ -48,10 +42,15 @@
class BpfExistenceTest : public ::testing::Test {
};
-//ToDo: replace isAtLeast25Q2 with IsAtLeastB once sdk_level have been upgraded to 36 on aosp/main
const bool unreleased = (android::base::GetProperty("ro.build.version.codename", "REL") != "REL");
-const int api_level = unreleased ? __ANDROID_API_FUTURE__ : android_get_device_api_level();
-const bool isAtLeast25Q2 = (api_level > __ANDROID_API_V__);
+const int api_level = unreleased ? 10000 : android_get_device_api_level();
+const bool isAtLeastR = (api_level >= 30);
+const bool isAtLeastS = (api_level >= 31);
+// Sv2 is 32
+const bool isAtLeastT = (api_level >= 33);
+const bool isAtLeastU = (api_level >= 34);
+const bool isAtLeastV = (api_level >= 35);
+const bool isAtLeast25Q2 = (api_level >= 36);
// Part of Android R platform (for 4.9+), but mainlined in S
static const set<string> PLATFORM_ONLY_IN_R = {
@@ -194,33 +193,33 @@
// and for the presence of mainline stuff.
// Note: Q is no longer supported by mainline
- ASSERT_TRUE(IsAtLeastR());
+ ASSERT_TRUE(isAtLeastR);
// R can potentially run on pre-4.9 kernel non-eBPF capable devices.
- DO_EXPECT(IsAtLeastR() && !IsAtLeastS() && isAtLeastKernelVersion(4, 9, 0), PLATFORM_ONLY_IN_R);
+ DO_EXPECT(isAtLeastR && !isAtLeastS && isAtLeastKernelVersion(4, 9, 0), PLATFORM_ONLY_IN_R);
// S requires Linux Kernel 4.9+ and thus requires eBPF support.
- if (IsAtLeastS()) ASSERT_TRUE(isAtLeastKernelVersion(4, 9, 0));
- DO_EXPECT(IsAtLeastS(), MAINLINE_FOR_S_PLUS);
+ if (isAtLeastS) ASSERT_TRUE(isAtLeastKernelVersion(4, 9, 0));
+ DO_EXPECT(isAtLeastS, MAINLINE_FOR_S_PLUS);
// Nothing added or removed in SCv2.
// T still only requires Linux Kernel 4.9+.
- DO_EXPECT(IsAtLeastT(), MAINLINE_FOR_T_PLUS);
- DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(4, 14, 0), MAINLINE_FOR_T_4_14_PLUS);
- DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(4, 19, 0), MAINLINE_FOR_T_4_19_PLUS);
- DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(5, 10, 0), MAINLINE_FOR_T_5_10_PLUS);
- DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(5, 15, 0), MAINLINE_FOR_T_5_15_PLUS);
+ DO_EXPECT(isAtLeastT, MAINLINE_FOR_T_PLUS);
+ DO_EXPECT(isAtLeastT && isAtLeastKernelVersion(4, 14, 0), MAINLINE_FOR_T_4_14_PLUS);
+ DO_EXPECT(isAtLeastT && isAtLeastKernelVersion(4, 19, 0), MAINLINE_FOR_T_4_19_PLUS);
+ DO_EXPECT(isAtLeastT && isAtLeastKernelVersion(5, 10, 0), MAINLINE_FOR_T_5_10_PLUS);
+ DO_EXPECT(isAtLeastT && isAtLeastKernelVersion(5, 15, 0), MAINLINE_FOR_T_5_15_PLUS);
// U requires Linux Kernel 4.14+, but nothing (as yet) added or removed in U.
- if (IsAtLeastU()) ASSERT_TRUE(isAtLeastKernelVersion(4, 14, 0));
- DO_EXPECT(IsAtLeastU(), MAINLINE_FOR_U_PLUS);
- DO_EXPECT(IsAtLeastU() && isAtLeastKernelVersion(5, 10, 0), MAINLINE_FOR_U_5_10_PLUS);
+ if (isAtLeastU) ASSERT_TRUE(isAtLeastKernelVersion(4, 14, 0));
+ DO_EXPECT(isAtLeastU, MAINLINE_FOR_U_PLUS);
+ DO_EXPECT(isAtLeastU && isAtLeastKernelVersion(5, 10, 0), MAINLINE_FOR_U_5_10_PLUS);
// V requires Linux Kernel 4.19+, but nothing (as yet) added or removed in V.
- if (IsAtLeastV()) ASSERT_TRUE(isAtLeastKernelVersion(4, 19, 0));
- DO_EXPECT(IsAtLeastV(), MAINLINE_FOR_V_PLUS);
- DO_EXPECT(IsAtLeastV() && isAtLeastKernelVersion(5, 4, 0), MAINLINE_FOR_V_5_4_PLUS);
+ if (isAtLeastV) ASSERT_TRUE(isAtLeastKernelVersion(4, 19, 0));
+ DO_EXPECT(isAtLeastV, MAINLINE_FOR_V_PLUS);
+ DO_EXPECT(isAtLeastV && isAtLeastKernelVersion(5, 4, 0), MAINLINE_FOR_V_5_4_PLUS);
if (isAtLeast25Q2) ASSERT_TRUE(isAtLeastKernelVersion(5, 4, 0));
DO_EXPECT(isAtLeast25Q2, MAINLINE_FOR_25Q2_PLUS);
diff --git a/framework/Android.bp b/framework/Android.bp
index d6ee759..f66bc60 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -162,7 +162,6 @@
srcs: [":httpclient_api_sources"],
static_libs: [
"com.android.net.http.flags-aconfig-java",
- "modules-utils-expresslog",
],
libs: [
"androidx.annotation_annotation",
diff --git a/framework/src/android/net/TestNetworkManager.java b/framework/src/android/net/TestNetworkManager.java
index 416c6de..cbc7a4f 100644
--- a/framework/src/android/net/TestNetworkManager.java
+++ b/framework/src/android/net/TestNetworkManager.java
@@ -23,8 +23,10 @@
import android.os.IBinder;
import android.os.RemoteException;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.List;
import java.util.Objects;
/**
@@ -196,45 +198,6 @@
}
/**
- * Create a tap interface for testing purposes
- *
- * @param linkAddrs an array of LinkAddresses to assign to the TAP interface
- * @return A TestNetworkInterface representing the underlying TAP interface. Close the contained
- * ParcelFileDescriptor to tear down the TAP interface.
- * @hide
- */
- @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
- @NonNull
- public TestNetworkInterface createTapInterface(@NonNull LinkAddress[] linkAddrs) {
- try {
- return mService.createInterface(TAP, CARRIER_UP, BRING_UP, USE_IPV6_PROV_DELAY,
- linkAddrs, null /* iface */);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- }
-
- /**
- * Create a tap interface for testing purposes
- *
- * @param bringUp whether to bring up the interface before returning it.
- *
- * @return A ParcelFileDescriptor of the underlying TAP interface. Close this to tear down the
- * TAP interface.
- * @hide
- */
- @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
- @NonNull
- public TestNetworkInterface createTapInterface(boolean bringUp) {
- try {
- return mService.createInterface(TAP, CARRIER_UP, bringUp, USE_IPV6_PROV_DELAY,
- NO_ADDRS, null /* iface */);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- }
-
- /**
* Create a tap interface with a given interface name for testing purposes
*
* @param bringUp whether to bring up the interface before returning it.
@@ -258,26 +221,6 @@
}
/**
- * Create a tap interface with or without carrier for testing purposes.
- *
- * Note: setting carrierUp = false is not supported until kernel version 6.0.
- *
- * @param carrierUp whether the created interface has a carrier or not.
- * @param bringUp whether to bring up the interface before returning it.
- * @hide
- */
- @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
- @NonNull
- public TestNetworkInterface createTapInterface(boolean carrierUp, boolean bringUp) {
- try {
- return mService.createInterface(TAP, carrierUp, bringUp, USE_IPV6_PROV_DELAY, NO_ADDRS,
- null /* iface */);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- }
-
- /**
* Create a tap interface for testing purposes.
*
* Note: setting carrierUp = false is not supported until kernel version 6.0.
@@ -300,27 +243,6 @@
}
/**
- * Create a tap interface for testing purposes.
- *
- * @param disableIpv6ProvisioningDelay whether to disable DAD and RS delay.
- * @param linkAddrs an array of LinkAddresses to assign to the TAP interface
- * @return A TestNetworkInterface representing the underlying TAP interface. Close the contained
- * ParcelFileDescriptor to tear down the TAP interface.
- * @hide
- */
- @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
- @NonNull
- public TestNetworkInterface createTapInterface(boolean disableIpv6ProvisioningDelay,
- @NonNull LinkAddress[] linkAddrs) {
- try {
- return mService.createInterface(TAP, CARRIER_UP, BRING_UP, disableIpv6ProvisioningDelay,
- linkAddrs, null /* iface */);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- }
-
- /**
* Enable / disable carrier on TestNetworkInterface
*
* Note: TUNSETCARRIER is not supported until kernel version 5.0.
@@ -337,4 +259,110 @@
throw e.rethrowFromSystemServer();
}
}
+
+ /**
+ * Represents a request to create a tun/tap interface for testing.
+ *
+ * @hide
+ */
+ public static class TestInterfaceRequest {
+ public final boolean isTun;
+ public final boolean hasCarrier;
+ public final boolean bringUp;
+ public final boolean disableIpv6ProvDelay;
+ @Nullable public final String ifname;
+ public final LinkAddress[] linkAddresses;
+
+ private TestInterfaceRequest(boolean isTun, boolean hasCarrier, boolean bringUp,
+ boolean disableProvDelay, @Nullable String ifname, LinkAddress[] linkAddresses) {
+ this.isTun = isTun;
+ this.hasCarrier = hasCarrier;
+ this.bringUp = bringUp;
+ this.disableIpv6ProvDelay = disableProvDelay;
+ this.ifname = ifname;
+ this.linkAddresses = linkAddresses;
+ }
+
+ /**
+ * Builder class for TestInterfaceRequest
+ *
+ * Defaults to a tap interface with carrier that has been brought up.
+ */
+ public static class Builder {
+ private boolean mIsTun = false;
+ private boolean mHasCarrier = true;
+ private boolean mBringUp = true;
+ private boolean mDisableIpv6ProvDelay = false;
+ @Nullable private String mIfname;
+ private List<LinkAddress> mLinkAddresses = new ArrayList<>();
+
+ /** Create tun interface. */
+ public Builder setTun() {
+ mIsTun = true;
+ return this;
+ }
+
+ /** Create tap interface. */
+ public Builder setTap() {
+ mIsTun = false;
+ return this;
+ }
+
+ /** Configure whether the interface has carrier. */
+ public Builder setHasCarrier(boolean hasCarrier) {
+ mHasCarrier = hasCarrier;
+ return this;
+ }
+
+ /** Configure whether the interface should be brought up. */
+ public Builder setBringUp(boolean bringUp) {
+ mBringUp = bringUp;
+ return this;
+ }
+
+ /** Disable DAD and RS delay. */
+ public Builder setDisableIpv6ProvisioningDelay(boolean disableProvDelay) {
+ mDisableIpv6ProvDelay = disableProvDelay;
+ return this;
+ }
+
+ /** Set the interface name. */
+ public Builder setInterfaceName(@Nullable String ifname) {
+ mIfname = ifname;
+ return this;
+ }
+
+ /** The addresses to configure on the interface. */
+ public Builder addLinkAddress(LinkAddress la) {
+ mLinkAddresses.add(la);
+ return this;
+ }
+
+ /** Build TestInterfaceRequest */
+ public TestInterfaceRequest build() {
+ return new TestInterfaceRequest(mIsTun, mHasCarrier, mBringUp,
+ mDisableIpv6ProvDelay, mIfname, mLinkAddresses.toArray(new LinkAddress[0]));
+ }
+ }
+ }
+
+ /**
+ * Create a TestNetworkInterface (tun or tap) for testing purposes.
+ *
+ * @param request The request describing the interface to create.
+ * @return A TestNetworkInterface representing the underlying tun/tap interface. Close the
+ * contained ParcelFileDescriptor to tear down the tun/tap interface.
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
+ @NonNull
+ public TestNetworkInterface createTestInterface(@NonNull TestInterfaceRequest request) {
+ try {
+ // TODO: Make TestInterfaceRequest parcelable and pass it instead.
+ return mService.createInterface(request.isTun, request.hasCarrier, request.bringUp,
+ request.disableIpv6ProvDelay, request.linkAddresses, request.ifname);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
}
diff --git a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
index 0536263..317854b 100644
--- a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
+++ b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
@@ -143,7 +143,7 @@
* @hide
*/
@ChangeId
- @EnabledSince(targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT)
+ @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT)
public static final long RESTRICT_LOCAL_NETWORK = 365139289L;
private ConnectivityCompatChanges() {
diff --git a/networksecurity/TEST_MAPPING b/networksecurity/TEST_MAPPING
index 448ee84..f75bf9a 100644
--- a/networksecurity/TEST_MAPPING
+++ b/networksecurity/TEST_MAPPING
@@ -1,4 +1,9 @@
{
+ "tethering-mainline-presubmit": [
+ {
+ "name": "NetworkSecurityUnitTests"
+ }
+ ],
"presubmit": [
{
"name": "CtsNetSecConfigCertificateTransparencyTestCases"
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index a8e3203..5c5f4ca 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -183,15 +183,17 @@
import com.android.net.module.util.NetworkStatsUtils;
import com.android.net.module.util.PermissionUtils;
import com.android.net.module.util.SharedLog;
+import com.android.net.module.util.SkDestroyListener;
import com.android.net.module.util.Struct;
import com.android.net.module.util.Struct.S32;
import com.android.net.module.util.Struct.U8;
import com.android.net.module.util.bpf.CookieTagMapKey;
import com.android.net.module.util.bpf.CookieTagMapValue;
+import com.android.net.module.util.netlink.InetDiagMessage;
+import com.android.net.module.util.netlink.StructInetDiagSockId;
import com.android.networkstack.apishim.BroadcastOptionsShimImpl;
import com.android.networkstack.apishim.ConstantsShim;
import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
-import com.android.server.BpfNetMaps;
import com.android.server.connectivity.ConnectivityResources;
import java.io.File;
@@ -216,6 +218,7 @@
import java.util.concurrent.Executor;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
/**
* Collect and persist detailed network statistics, and provide this data to
@@ -726,12 +729,14 @@
mTrafficStatsUidCache = null;
}
- // TODO: Remove bpfNetMaps creation and always start SkDestroyListener
- // Following code is for the experiment to verify the SkDestroyListener refactoring. Based
- // on the experiment flag, BpfNetMaps starts C SkDestroyListener (existing code) or
- // NetworkStatsService starts Java SkDestroyListener (new code).
- final BpfNetMaps bpfNetMaps = mDeps.makeBpfNetMaps(mContext);
- mSkDestroyListener = mDeps.makeSkDestroyListener(mCookieTagMap, mHandler);
+ mSkDestroyListener = mDeps.makeSkDestroyListener((message) -> {
+ final StructInetDiagSockId sockId = message.inetDiagMsg.id;
+ try {
+ mCookieTagMap.deleteEntry(new CookieTagMapKey(sockId.cookie));
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to delete CookieTagMap entry for " + sockId.cookie + ": " + e);
+ }
+ }, mHandler);
mHandler.post(mSkDestroyListener::start);
}
@@ -952,16 +957,11 @@
return Build.isDebuggable();
}
- /** Create a new BpfNetMaps. */
- public BpfNetMaps makeBpfNetMaps(Context ctx) {
- return new BpfNetMaps(ctx);
- }
-
/** Create a new SkDestroyListener. */
- public SkDestroyListener makeSkDestroyListener(
- IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap, Handler handler) {
- return new SkDestroyListener(
- cookieTagMap, handler, new SharedLog(MAX_SOCKET_DESTROY_LISTENER_LOGS, TAG));
+ public SkDestroyListener makeSkDestroyListener(Consumer<InetDiagMessage> consumer,
+ Handler handler) {
+ return SkDestroyListener.makeSkDestroyListener(consumer, handler,
+ new SharedLog(MAX_SOCKET_DESTROY_LISTENER_LOGS, TAG));
}
/**
diff --git a/service-t/src/com/android/server/net/SkDestroyListener.java b/service-t/src/com/android/server/net/SkDestroyListener.java
deleted file mode 100644
index a6cc2b5..0000000
--- a/service-t/src/com/android/server/net/SkDestroyListener.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * 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 com.android.server.net;
-
-import static android.system.OsConstants.NETLINK_INET_DIAG;
-
-import android.os.Handler;
-import android.system.ErrnoException;
-
-import com.android.net.module.util.IBpfMap;
-import com.android.net.module.util.SharedLog;
-import com.android.net.module.util.bpf.CookieTagMapKey;
-import com.android.net.module.util.bpf.CookieTagMapValue;
-import com.android.net.module.util.ip.NetlinkMonitor;
-import com.android.net.module.util.netlink.InetDiagMessage;
-import com.android.net.module.util.netlink.NetlinkMessage;
-import com.android.net.module.util.netlink.StructInetDiagSockId;
-
-import java.io.PrintWriter;
-
-/**
- * Monitor socket destroy and delete entry from cookie tag bpf map.
- */
-public class SkDestroyListener extends NetlinkMonitor {
- private static final int SKNLGRP_INET_TCP_DESTROY = 1;
- private static final int SKNLGRP_INET_UDP_DESTROY = 2;
- private static final int SKNLGRP_INET6_TCP_DESTROY = 3;
- private static final int SKNLGRP_INET6_UDP_DESTROY = 4;
-
- // TODO: if too many sockets are closed too quickly, this can overflow the socket buffer, and
- // some entries in mCookieTagMap will not be freed. In order to fix this it would be needed to
- // periodically dump all sockets and remove the tag entries for sockets that have been closed.
- // For now, set a large-enough buffer that hundreds of sockets can be closed without getting
- // ENOBUFS and leaking mCookieTagMap entries.
- private static final int SOCK_RCV_BUF_SIZE = 512 * 1024;
-
- private final IBpfMap<CookieTagMapKey, CookieTagMapValue> mCookieTagMap;
-
- SkDestroyListener(final IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap,
- final Handler handler, final SharedLog log) {
- super(handler, log, "SkDestroyListener", NETLINK_INET_DIAG,
- 1 << (SKNLGRP_INET_TCP_DESTROY - 1)
- | 1 << (SKNLGRP_INET_UDP_DESTROY - 1)
- | 1 << (SKNLGRP_INET6_TCP_DESTROY - 1)
- | 1 << (SKNLGRP_INET6_UDP_DESTROY - 1),
- SOCK_RCV_BUF_SIZE);
- mCookieTagMap = cookieTagMap;
- }
-
- @Override
- public void processNetlinkMessage(final NetlinkMessage nlMsg, final long whenMs) {
- if (!(nlMsg instanceof InetDiagMessage)) {
- mLog.e("Received non InetDiagMessage");
- return;
- }
- final StructInetDiagSockId sockId = ((InetDiagMessage) nlMsg).inetDiagMsg.id;
- try {
- mCookieTagMap.deleteEntry(new CookieTagMapKey(sockId.cookie));
- } catch (ErrnoException e) {
- mLog.e("Failed to delete CookieTagMap entry for " + sockId.cookie + ": " + e);
- }
- }
-
- /**
- * Dump the contents of SkDestroyListener log.
- */
- public void dump(PrintWriter pw) {
- mLog.reverseDump(pw);
- }
-}
diff --git a/service/libconnectivity/include/connectivity_native.h b/service/libconnectivity/include/connectivity_native.h
index f4676a9..f264b68 100644
--- a/service/libconnectivity/include/connectivity_native.h
+++ b/service/libconnectivity/include/connectivity_native.h
@@ -20,12 +20,6 @@
#include <sys/cdefs.h>
#include <netinet/in.h>
-// For branches that do not yet have __ANDROID_API_U__ defined, like module
-// release branches.
-#ifndef __ANDROID_API_U__
-#define __ANDROID_API_U__ 34
-#endif
-
__BEGIN_DECLS
/**
@@ -41,7 +35,7 @@
*
* @param port Int corresponding to port number.
*/
-int AConnectivityNative_blockPortForBind(in_port_t port) __INTRODUCED_IN(__ANDROID_API_U__);
+int AConnectivityNative_blockPortForBind(in_port_t port) __INTRODUCED_IN(34);
/**
* Unblocks a port that has previously been blocked.
@@ -54,7 +48,7 @@
*
* @param port Int corresponding to port number.
*/
-int AConnectivityNative_unblockPortForBind(in_port_t port) __INTRODUCED_IN(__ANDROID_API_U__);
+int AConnectivityNative_unblockPortForBind(in_port_t port) __INTRODUCED_IN(34);
/**
* Unblocks all ports that have previously been blocked.
@@ -64,7 +58,7 @@
* - EPERM if the UID of the client doesn't have network stack permission
* - Other errors as per https://man7.org/linux/man-pages/man2/bpf.2.html
*/
-int AConnectivityNative_unblockAllPortsForBind() __INTRODUCED_IN(__ANDROID_API_U__);
+int AConnectivityNative_unblockAllPortsForBind() __INTRODUCED_IN(34);
/**
* Gets the list of ports that have been blocked.
@@ -79,7 +73,7 @@
* blocked ports, which may be larger than the ports array that was filled.
*/
int AConnectivityNative_getPortsBlockedForBind(in_port_t* _Nonnull ports, size_t* _Nonnull count)
- __INTRODUCED_IN(__ANDROID_API_U__);
+ __INTRODUCED_IN(34);
__END_DECLS
diff --git a/service/native/libs/libclat/Android.bp b/service/native/libs/libclat/Android.bp
index 6c1c2c4..9554bd8 100644
--- a/service/native/libs/libclat/Android.bp
+++ b/service/native/libs/libclat/Android.bp
@@ -47,6 +47,7 @@
srcs: [
"clatutils_test.cpp",
],
+ stl: "libc++_static",
header_libs: [
"bpf_connectivity_headers",
],
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index ebb8de1..7c0c223 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -38,16 +38,16 @@
import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
-import static android.net.INetd.PERMISSION_INTERNET;
-import static android.net.INetd.PERMISSION_NONE;
-import static android.net.INetd.PERMISSION_UNINSTALLED;
-import static android.net.INetd.PERMISSION_UPDATE_DEVICE_STATS;
import static android.system.OsConstants.EINVAL;
import static android.system.OsConstants.ENODEV;
import static android.system.OsConstants.ENOENT;
import static android.system.OsConstants.EOPNOTSUPP;
import static com.android.server.ConnectivityStatsLog.NETWORK_BPF_MAP_INFO;
+import static com.android.server.connectivity.NetworkPermissions.PERMISSION_NONE;
+import static com.android.server.connectivity.NetworkPermissions.TRAFFIC_PERMISSION_INTERNET;
+import static com.android.server.connectivity.NetworkPermissions.TRAFFIC_PERMISSION_UNINSTALLED;
+import static com.android.server.connectivity.NetworkPermissions.TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS;
import android.app.StatsManager;
import android.content.Context;
@@ -139,8 +139,8 @@
private static IBpfMap<U32, Bool> sLocalNetBlockedUidMap = null;
private static final List<Pair<Integer, String>> PERMISSION_LIST = Arrays.asList(
- Pair.create(PERMISSION_INTERNET, "PERMISSION_INTERNET"),
- Pair.create(PERMISSION_UPDATE_DEVICE_STATS, "PERMISSION_UPDATE_DEVICE_STATS")
+ Pair.create(TRAFFIC_PERMISSION_INTERNET, "PERMISSION_INTERNET"),
+ Pair.create(TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS, "PERMISSION_UPDATE_DEVICE_STATS")
);
/**
@@ -870,7 +870,8 @@
}
// Remove the entry if package is uninstalled or uid has only INTERNET permission.
- if (permissions == PERMISSION_UNINSTALLED || permissions == PERMISSION_INTERNET) {
+ if (permissions == TRAFFIC_PERMISSION_UNINSTALLED
+ || permissions == TRAFFIC_PERMISSION_INTERNET) {
for (final int uid : uids) {
try {
sUidPermissionMap.deleteEntry(new S32(uid));
@@ -1043,10 +1044,10 @@
// Key of uid permission map is appId
// TODO: Rename map name
final U8 permissions = sUidPermissionMap.getValue(new S32(appId));
- return permissions != null ? permissions.val : PERMISSION_INTERNET;
+ return permissions != null ? permissions.val : TRAFFIC_PERMISSION_INTERNET;
} catch (ErrnoException e) {
Log.wtf(TAG, "Failed to get permission for uid: " + uid);
- return PERMISSION_INTERNET;
+ return TRAFFIC_PERMISSION_INTERNET;
}
}
@@ -1195,7 +1196,7 @@
if (permissionMask == PERMISSION_NONE) {
return "PERMISSION_NONE";
}
- if (permissionMask == PERMISSION_UNINSTALLED) {
+ if (permissionMask == TRAFFIC_PERMISSION_UNINSTALLED) {
// PERMISSION_UNINSTALLED should never appear in the map
return "PERMISSION_UNINSTALLED error!";
}
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 5b415c8..6dbb1d8 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -564,6 +564,7 @@
// The Context is created for UserHandle.ALL.
private final Context mUserAllContext;
private final Dependencies mDeps;
+ private final PermissionMonitor.Dependencies mPermissionMonitorDeps;
private final ConnectivityFlags mFlags;
// 0 is full bad, 100 is full good
private int mDefaultInetConditionPublished = 0;
@@ -1023,6 +1024,8 @@
private final LingerMonitor mLingerMonitor;
private final SatelliteAccessController mSatelliteAccessController;
+ private final L2capNetworkProvider mL2capNetworkProvider;
+
// sequence number of NetworkRequests
private int mNextNetworkRequestId = NetworkRequest.FIRST_REQUEST_ID;
@@ -1623,6 +1626,11 @@
connectivityServiceInternalHandler);
}
+ /** Creates an L2capNetworkProvider */
+ public L2capNetworkProvider makeL2capNetworkProvider(Context context) {
+ return new L2capNetworkProvider(context);
+ }
+
/** Returns the data inactivity timeout to be used for cellular networks */
public int getDefaultCellularDataInactivityTimeout() {
return DeviceConfigUtils.getDeviceConfigPropertyInt(NAMESPACE_TETHERING_BOOT,
@@ -1801,12 +1809,13 @@
public ConnectivityService(Context context) {
this(context, getDnsResolver(context), new IpConnectivityLog(),
INetd.Stub.asInterface((IBinder) context.getSystemService(Context.NETD_SERVICE)),
- new Dependencies());
+ new Dependencies(), new PermissionMonitor.Dependencies());
}
@VisibleForTesting
protected ConnectivityService(Context context, IDnsResolver dnsresolver,
- IpConnectivityLog logger, INetd netd, Dependencies deps) {
+ IpConnectivityLog logger, INetd netd, Dependencies deps,
+ PermissionMonitor.Dependencies mPermDeps) {
if (DBG) log("ConnectivityService starting up");
mDeps = Objects.requireNonNull(deps, "missing Dependencies");
@@ -1879,8 +1888,10 @@
mNetd = netd;
mBpfNetMaps = mDeps.getBpfNetMaps(mContext, netd);
mHandlerThread = mDeps.makeHandlerThread("ConnectivityServiceThread");
+ mPermissionMonitorDeps = mPermDeps;
mPermissionMonitor =
- new PermissionMonitor(mContext, mNetd, mBpfNetMaps, mHandlerThread);
+ new PermissionMonitor(mContext, mNetd, mBpfNetMaps, mPermissionMonitorDeps,
+ mHandlerThread);
mHandlerThread.start();
mHandler = new InternalHandler(mHandlerThread.getLooper());
mTrackerHandler = new NetworkStateTrackerHandler(mHandlerThread.getLooper());
@@ -2094,6 +2105,8 @@
}
mIngressToVpnAddressFiltering = mDeps.isAtLeastT()
&& mDeps.isFeatureNotChickenedOut(mContext, INGRESS_TO_VPN_ADDRESS_FILTERING);
+
+ mL2capNetworkProvider = mDeps.makeL2capNetworkProvider(mContext);
}
/**
@@ -4129,6 +4142,10 @@
mCarrierPrivilegeAuthenticator.start();
}
+ if (mL2capNetworkProvider != null) {
+ mL2capNetworkProvider.start();
+ }
+
// On T+ devices, register callback for statsd to pull NETWORK_BPF_MAP_INFO atom
if (mDeps.isAtLeastT()) {
mBpfNetMaps.setPullAtomCallback(mContext);
@@ -9791,42 +9808,61 @@
removeLocalAddressesFromBpfMap(iface, MULTICAST_AND_BROADCAST_PREFIXES, oldLp);
}
- final CompareResult<LinkAddress> linkAddressDiff = new CompareResult<>(
- oldLp != null ? oldLp.getLinkAddresses() : null,
- newLp != null ? newLp.getLinkAddresses() : null);
+ // The both list contain current link properties + stacked links for new and old LP.
+ List<LinkProperties> newLinkProperties = new ArrayList<>();
+ List<LinkProperties> oldLinkProperties = new ArrayList<>();
- List<IpPrefix> unicastLocalPrefixesToBeAdded = new ArrayList<>();
- List<IpPrefix> unicastLocalPrefixesToBeRemoved = new ArrayList<>();
-
- // Finding the list of local network prefixes that needs to be added
if (newLp != null) {
- for (LinkAddress linkAddress : newLp.getLinkAddresses()) {
+ newLinkProperties.add(newLp);
+ newLinkProperties.addAll(newLp.getStackedLinks());
+ }
+ if (oldLp != null) {
+ oldLinkProperties.add(oldLp);
+ oldLinkProperties.addAll(oldLp.getStackedLinks());
+ }
+
+ // map contains interface name to list of local network prefixes added because of change
+ // in link properties
+ Map<String, List<IpPrefix>> prefixesAddedForInterface = new ArrayMap<>();
+
+ final CompareResult<LinkProperties> linkPropertiesDiff = new CompareResult<>(
+ oldLinkProperties, newLinkProperties);
+
+ for (LinkProperties linkProperty : linkPropertiesDiff.added) {
+ List<IpPrefix> unicastLocalPrefixesToBeAdded = new ArrayList<>();
+ for (LinkAddress linkAddress : linkProperty.getLinkAddresses()) {
unicastLocalPrefixesToBeAdded.addAll(
getLocalNetworkPrefixesForAddress(linkAddress));
}
- }
+ addLocalAddressesToBpfMap(linkProperty.getInterfaceName(),
+ unicastLocalPrefixesToBeAdded, linkProperty);
- for (LinkAddress linkAddress : linkAddressDiff.removed) {
- unicastLocalPrefixesToBeRemoved.addAll(getLocalNetworkPrefixesForAddress(linkAddress));
- }
-
- // If newLp is not null, adding local network prefixes using interface name of newLp
- if (newLp != null) {
- addLocalAddressesToBpfMap(newLp.getInterfaceName(),
- new ArrayList<>(unicastLocalPrefixesToBeAdded), newLp);
- }
- if (oldLp != null) {
- // excluding removal of ip prefixes that needs to be added for newLp, but also
- // removed for oldLp.
- if (newLp != null && Objects.equals(oldLp.getInterfaceName(),
- newLp.getInterfaceName())) {
- unicastLocalPrefixesToBeRemoved.removeAll(unicastLocalPrefixesToBeAdded);
+ // adding iterface name -> ip prefixes that we added to map
+ if (!prefixesAddedForInterface.containsKey(linkProperty.getInterfaceName())) {
+ prefixesAddedForInterface.put(linkProperty.getInterfaceName(), new ArrayList<>());
}
- // removing ip local network prefixes because of change in link addresses.
- removeLocalAddressesFromBpfMap(oldLp.getInterfaceName(),
- new ArrayList<>(unicastLocalPrefixesToBeRemoved), oldLp);
+ prefixesAddedForInterface.get(linkProperty.getInterfaceName())
+ .addAll(unicastLocalPrefixesToBeAdded);
}
+ for (LinkProperties linkProperty : linkPropertiesDiff.removed) {
+ List<IpPrefix> unicastLocalPrefixesToBeRemoved = new ArrayList<>();
+ List<IpPrefix> unicastLocalPrefixesAdded = prefixesAddedForInterface.getOrDefault(
+ linkProperty.getInterfaceName(), new ArrayList<>());
+
+ for (LinkAddress linkAddress : linkProperty.getLinkAddresses()) {
+ unicastLocalPrefixesToBeRemoved.addAll(
+ getLocalNetworkPrefixesForAddress(linkAddress));
+ }
+
+ // This is to ensure if 10.0.10.0/24 was added and 10.0.11.0/24 was removed both will
+ // still populate the same prefix of 10.0.0.0/8, which mean we should not remove the
+ // prefix because of removal of 10.0.11.0/24
+ unicastLocalPrefixesToBeRemoved.removeAll(unicastLocalPrefixesAdded);
+
+ removeLocalAddressesFromBpfMap(linkProperty.getInterfaceName(),
+ new ArrayList<>(unicastLocalPrefixesToBeRemoved), linkProperty);
+ }
}
/**
diff --git a/service/src/com/android/server/L2capNetworkProvider.java b/service/src/com/android/server/L2capNetworkProvider.java
index d0b0603..814a068 100644
--- a/service/src/com/android/server/L2capNetworkProvider.java
+++ b/service/src/com/android/server/L2capNetworkProvider.java
@@ -16,7 +16,7 @@
package com.android.server;
-import static android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_6LOWPAN;
+import static android.content.pm.PackageManager.FEATURE_BLUETOOTH_LE;
import static android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_ANY;
import static android.net.L2capNetworkSpecifier.PSM_ANY;
import static android.net.L2capNetworkSpecifier.ROLE_CLIENT;
@@ -30,7 +30,6 @@
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
import static android.net.NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS;
import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
-import static android.content.pm.PackageManager.FEATURE_BLUETOOTH_LE;
import static android.system.OsConstants.F_GETFL;
import static android.system.OsConstants.F_SETFL;
import static android.system.OsConstants.O_NONBLOCK;
@@ -50,10 +49,8 @@
import android.net.NetworkProvider.NetworkOfferCallback;
import android.net.NetworkRequest;
import android.net.NetworkScore;
-import android.net.NetworkSpecifier;
import android.os.Handler;
import android.os.HandlerThread;
-import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.system.Os;
import android.util.ArrayMap;
@@ -61,8 +58,10 @@
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.HandlerUtils;
import com.android.net.module.util.ServiceConnectivityJni;
import com.android.server.net.L2capNetwork;
+import com.android.server.net.L2capPacketForwarder;
import java.io.IOException;
import java.util.ArrayList;
@@ -78,7 +77,6 @@
// BLUETOOTH_CONNECT permission.
NetworkCapabilities.Builder.withoutDefaultCapabilities()
.addTransportType(TRANSPORT_BLUETOOTH)
- // TODO: remove NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED.
.addCapability(NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED)
.addCapability(NET_CAPABILITY_NOT_CONGESTED)
.addCapability(NET_CAPABILITY_NOT_METERED)
@@ -95,8 +93,9 @@
private final BlanketReservationOffer mBlanketOffer;
private final Set<ReservedServerOffer> mReservedServerOffers = new ArraySet<>();
private final ClientOffer mClientOffer;
- private final BluetoothManager mBluetoothManager;
- private final boolean mIsSupported;
+ // mBluetoothManager guaranteed non-null when read on handler thread after start() is called
+ @Nullable
+ private BluetoothManager mBluetoothManager;
// Note: IFNAMSIZ is 16.
private static final String TUN_IFNAME = "l2cap-tun";
@@ -110,12 +109,7 @@
* requests that do not have a NetworkSpecifier set.
*/
private class BlanketReservationOffer implements NetworkOfferCallback {
- // Set as transport primary to ensure that the BlanketReservationOffer always outscores the
- // ReservedServerOffer, because as soon as the BlanketReservationOffer receives an
- // onNetworkUnneeded() callback, it will tear down the associated reserved offer.
- public static final NetworkScore SCORE = new NetworkScore.Builder()
- .setTransportPrimary(true)
- .build();
+ public static final NetworkScore SCORE = new NetworkScore.Builder().build();
// Note the missing NET_CAPABILITY_NOT_RESTRICTED marking the network as restricted.
public static final NetworkCapabilities CAPABILITIES;
static {
@@ -133,12 +127,7 @@
}
// TODO: consider moving this into L2capNetworkSpecifier as #isValidServerReservation().
- private boolean isValidL2capSpecifier(@Nullable NetworkSpecifier spec) {
- if (spec == null) return false;
- // If spec is not null, L2capNetworkSpecifier#canBeSatisfiedBy() guarantees the
- // specifier is of type L2capNetworkSpecifier.
- final L2capNetworkSpecifier l2capSpec = (L2capNetworkSpecifier) spec;
-
+ private boolean isValidL2capServerSpecifier(L2capNetworkSpecifier l2capSpec) {
// The ROLE_SERVER offer can be satisfied by a ROLE_ANY request.
if (l2capSpec.getRole() != ROLE_SERVER) return false;
@@ -156,9 +145,13 @@
@Override
public void onNetworkNeeded(NetworkRequest request) {
- Log.d(TAG, "New reservation request: " + request);
- if (!isValidL2capSpecifier(request.getNetworkSpecifier())) {
- Log.w(TAG, "Ignoring invalid reservation request: " + request);
+ // The NetworkSpecifier is guaranteed to be either null or an L2capNetworkSpecifier, so
+ // this cast is safe.
+ final L2capNetworkSpecifier specifier =
+ (L2capNetworkSpecifier) request.getNetworkSpecifier();
+ if (specifier == null) return;
+ if (!isValidL2capServerSpecifier(specifier)) {
+ Log.i(TAG, "Ignoring invalid reservation request: " + request);
return;
}
@@ -234,6 +227,7 @@
}
private void destroyAndUnregisterReservedOffer(ReservedServerOffer reservedOffer) {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
// Ensure the offer still exists if this was posted on the handler.
if (!mReservedServerOffers.contains(reservedOffer)) return;
mReservedServerOffers.remove(reservedOffer);
@@ -243,36 +237,17 @@
}
@Nullable
- private static ParcelFileDescriptor createTunInterface(String ifname) {
- final ParcelFileDescriptor fd;
- try {
- fd = ParcelFileDescriptor.adoptFd(
- ServiceConnectivityJni.createTunTap(
- true /*isTun*/, true /*hasCarrier*/, true /*setIffMulticast*/, ifname));
- ServiceConnectivityJni.bringUpInterface(ifname);
- // TODO: consider adding a parameter to createTunTap() (or the Builder that should
- // be added) to configure i/o blocking.
- final int flags = Os.fcntlInt(fd.getFileDescriptor(), F_GETFL, 0);
- Os.fcntlInt(fd.getFileDescriptor(), F_SETFL, flags & ~O_NONBLOCK);
- } catch (Exception e) {
- // Note: createTunTap currently throws an IllegalStateException on failure.
- // TODO: native functions should throw ErrnoException.
- Log.e(TAG, "Failed to create tun interface", e);
- return null;
- }
- return fd;
- }
-
- @Nullable
private L2capNetwork createL2capNetwork(BluetoothSocket socket, NetworkCapabilities caps,
L2capNetwork.ICallback cb) {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
final String ifname = TUN_IFNAME + String.valueOf(sTunIndex++);
- final ParcelFileDescriptor tunFd = createTunInterface(ifname);
+ final ParcelFileDescriptor tunFd = mDeps.createTunInterface(ifname);
if (tunFd == null) {
return null;
}
- return new L2capNetwork(mHandler, mContext, mProvider, ifname, socket, tunFd, caps, cb);
+ return L2capNetwork.create(
+ mHandler, mContext, mProvider, ifname, socket, tunFd, caps, mDeps, cb);
}
private static void closeBluetoothSocket(BluetoothSocket socket) {
@@ -298,22 +273,26 @@
private volatile boolean mIsRunning = true;
public AcceptThread(BluetoothServerSocket serverSocket) {
+ super("L2capNetworkProvider-AcceptThread");
mServerSocket = serverSocket;
}
private void postDestroyAndUnregisterReservedOffer() {
+ // Called on AcceptThread
mHandler.post(() -> {
destroyAndUnregisterReservedOffer(ReservedServerOffer.this);
});
}
private void postCreateServerNetwork(BluetoothSocket connectedSocket) {
+ // Called on AcceptThread
mHandler.post(() -> {
final boolean success = createServerNetwork(connectedSocket);
if (!success) closeBluetoothSocket(connectedSocket);
});
}
+ @Override
public void run() {
while (mIsRunning) {
final BluetoothSocket connectedSocket;
@@ -333,6 +312,7 @@
}
public void tearDown() {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
mIsRunning = false;
try {
// BluetoothServerSocket.close() is thread-safe.
@@ -349,6 +329,7 @@
}
private boolean createServerNetwork(BluetoothSocket socket) {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
// It is possible the offer went away.
if (!mReservedServerOffers.contains(this)) return false;
@@ -359,17 +340,19 @@
final L2capNetwork network = createL2capNetwork(socket, mReservedCapabilities,
new L2capNetwork.ICallback() {
- @Override
- public void onError(L2capNetwork network) {
- destroyAndUnregisterReservedOffer(ReservedServerOffer.this);
- }
- @Override
- public void onNetworkUnwanted(L2capNetwork network) {
- // Leave reservation in place.
- final boolean networkExists = mL2capNetworks.remove(network);
- if (!networkExists) return; // already torn down.
- network.tearDown();
- }
+ @Override
+ public void onError(L2capNetwork network) {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+ destroyAndUnregisterReservedOffer(ReservedServerOffer.this);
+ }
+ @Override
+ public void onNetworkUnwanted(L2capNetwork network) {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+ // Leave reservation in place.
+ final boolean networkExists = mL2capNetworks.remove(network);
+ if (!networkExists) return; // already torn down.
+ network.tearDown();
+ }
});
if (network == null) {
@@ -404,6 +387,7 @@
/** Called when the reservation goes away and the reserved offer must be torn down. */
public void tearDown() {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
mAcceptThread.tearDown();
for (L2capNetwork network : mL2capNetworks) {
network.tearDown();
@@ -453,10 +437,12 @@
private volatile boolean mIsAborted = false;
public ConnectThread(L2capNetworkSpecifier specifier, BluetoothSocket socket) {
+ super("L2capNetworkProvider-ConnectThread");
mSpecifier = specifier;
mSocket = socket;
}
+ @Override
public void run() {
try {
mSocket.connect();
@@ -476,6 +462,7 @@
}
public void abort() {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
mIsAborted = true;
// Closing the BluetoothSocket is the only way to unblock connect() because it calls
// shutdown on the underlying (connected) SOCK_SEQPACKET.
@@ -491,6 +478,7 @@
private boolean createClientNetwork(L2capNetworkSpecifier specifier,
BluetoothSocket socket) {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
// Check whether request still exists
final ClientRequestInfo cri = mClientNetworkRequests.get(specifier);
if (cri == null) return false;
@@ -501,18 +489,20 @@
final L2capNetwork network = createL2capNetwork(socket, caps,
new L2capNetwork.ICallback() {
- // TODO: do not send onUnavailable() after the network has become available. The
- // right thing to do here is to tearDown the network (if it still exists, because
- // note that the request might have already been removed in the meantime, so
- // `network` cannot be used directly.
- @Override
- public void onError(L2capNetwork network) {
- declareAllNetworkRequestsUnfulfillable(specifier);
- }
- @Override
- public void onNetworkUnwanted(L2capNetwork network) {
- declareAllNetworkRequestsUnfulfillable(specifier);
- }
+ // TODO: do not send onUnavailable() after the network has become available. The
+ // right thing to do here is to tearDown the network (if it still exists,
+ // because note that the request might have already been removed in the
+ // meantime, so `network` cannot be used directly.
+ @Override
+ public void onError(L2capNetwork network) {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+ declareAllNetworkRequestsUnfulfillable(specifier);
+ }
+ @Override
+ public void onNetworkUnwanted(L2capNetwork network) {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+ declareAllNetworkRequestsUnfulfillable(specifier);
+ }
});
if (network == null) return false;
@@ -520,12 +510,7 @@
return true;
}
- private boolean isValidL2capSpecifier(@Nullable NetworkSpecifier spec) {
- if (spec == null) return false;
-
- // If not null, guaranteed to be L2capNetworkSepcifier.
- final L2capNetworkSpecifier l2capSpec = (L2capNetworkSpecifier) spec;
-
+ private boolean isValidL2capClientSpecifier(L2capNetworkSpecifier l2capSpec) {
// The ROLE_CLIENT offer can be satisfied by a ROLE_ANY request.
if (l2capSpec.getRole() != ROLE_CLIENT) return false;
@@ -545,14 +530,16 @@
@Override
public void onNetworkNeeded(NetworkRequest request) {
- Log.d(TAG, "New client network request: " + request);
- if (!isValidL2capSpecifier(request.getNetworkSpecifier())) {
- Log.w(TAG, "Ignoring invalid client request: " + request);
+ // The NetworkSpecifier is guaranteed to be either null or an L2capNetworkSpecifier, so
+ // this cast is safe.
+ final L2capNetworkSpecifier requestSpecifier =
+ (L2capNetworkSpecifier) request.getNetworkSpecifier();
+ if (requestSpecifier == null) return;
+ if (!isValidL2capClientSpecifier(requestSpecifier)) {
+ Log.i(TAG, "Ignoring invalid client request: " + request);
return;
}
- final L2capNetworkSpecifier requestSpecifier =
- (L2capNetworkSpecifier) request.getNetworkSpecifier();
// Check whether this exact request is already being tracked.
final ClientRequestInfo cri = mClientNetworkRequests.get(requestSpecifier);
if (cri != null) {
@@ -627,6 +614,7 @@
* Only call this when all associated NetworkRequests have been released.
*/
private void releaseClientNetworkRequest(ClientRequestInfo cri) {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
mClientNetworkRequests.remove(cri.specifier);
if (cri.connectThread.isAlive()) {
// Note that if ConnectThread succeeds between calling #isAlive() and #abort(), the
@@ -642,6 +630,7 @@
}
private void declareAllNetworkRequestsUnfulfillable(L2capNetworkSpecifier specifier) {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
final ClientRequestInfo cri = mClientNetworkRequests.get(specifier);
if (cri == null) return;
@@ -654,34 +643,57 @@
@VisibleForTesting
public static class Dependencies {
- /** Get NetworkProvider */
- public NetworkProvider getNetworkProvider(Context context, Looper looper) {
- return new NetworkProvider(context, looper, TAG);
- }
-
/** Get the HandlerThread for L2capNetworkProvider to run on */
public HandlerThread getHandlerThread() {
final HandlerThread thread = new HandlerThread("L2capNetworkProviderThread");
thread.start();
return thread;
}
+
+ /** Create a tun interface configured for blocking i/o */
+ @Nullable
+ public ParcelFileDescriptor createTunInterface(String ifname) {
+ final ParcelFileDescriptor fd;
+ try {
+ fd = ParcelFileDescriptor.adoptFd(ServiceConnectivityJni.createTunTap(
+ true /*isTun*/,
+ true /*hasCarrier*/,
+ true /*setIffMulticast*/,
+ ifname));
+ ServiceConnectivityJni.bringUpInterface(ifname);
+ // TODO: consider adding a parameter to createTunTap() (or the Builder that should
+ // be added) to configure i/o blocking.
+ final int flags = Os.fcntlInt(fd.getFileDescriptor(), F_GETFL, 0);
+ Os.fcntlInt(fd.getFileDescriptor(), F_SETFL, flags & ~O_NONBLOCK);
+ } catch (Exception e) {
+ // Note: createTunTap currently throws an IllegalStateException on failure.
+ // TODO: native functions should throw ErrnoException.
+ Log.e(TAG, "Failed to create tun interface", e);
+ return null;
+ }
+ return fd;
+ }
+
+ /** Create an L2capPacketForwarder and start forwarding */
+ public L2capPacketForwarder createL2capPacketForwarder(Handler handler,
+ ParcelFileDescriptor tunFd, BluetoothSocket socket, boolean compressHeaders,
+ L2capPacketForwarder.ICallback cb) {
+ return new L2capPacketForwarder(handler, tunFd, socket, compressHeaders, cb);
+ }
}
public L2capNetworkProvider(Context context) {
this(new Dependencies(), context);
}
- @VisibleForTesting
public L2capNetworkProvider(Dependencies deps, Context context) {
mDeps = deps;
mContext = context;
mHandlerThread = mDeps.getHandlerThread();
mHandler = new Handler(mHandlerThread.getLooper());
- mProvider = mDeps.getNetworkProvider(context, mHandlerThread.getLooper());
+ mProvider = new NetworkProvider(context, mHandlerThread.getLooper(), TAG);
mBlanketOffer = new BlanketReservationOffer();
mClientOffer = new ClientOffer();
- mBluetoothManager = context.getSystemService(BluetoothManager.class);
- mIsSupported = mContext.getPackageManager().hasSystemFeature(FEATURE_BLUETOOTH_LE);
}
/**
@@ -690,19 +702,17 @@
* Called on CS Handler thread.
*/
public void start() {
- if (!mIsSupported) {
- // In order to make mHandler final, the HandlerThread needs to be started before
- // HandlerThread.getLooper() is called during the construction of the Handler.
- mHandlerThread.quitSafely();
- try {
- mHandlerThread.join();
- } catch (InterruptedException e) {
- // join() interrupted. Do nothing.
- }
- return;
- }
-
mHandler.post(() -> {
+ final PackageManager pm = mContext.getPackageManager();
+ if (!pm.hasSystemFeature(FEATURE_BLUETOOTH_LE)) {
+ return;
+ }
+ mBluetoothManager = mContext.getSystemService(BluetoothManager.class);
+ if (mBluetoothManager == null) {
+ // Can this ever happen?
+ Log.wtf(TAG, "BluetoothManager not found");
+ return;
+ }
mContext.getSystemService(ConnectivityManager.class).registerNetworkProvider(mProvider);
mProvider.registerNetworkOffer(BlanketReservationOffer.SCORE,
BlanketReservationOffer.CAPABILITIES, mHandler::post, mBlanketOffer);
diff --git a/service/src/com/android/server/connectivity/NetworkPermissions.java b/service/src/com/android/server/connectivity/NetworkPermissions.java
new file mode 100644
index 0000000..9543d8f
--- /dev/null
+++ b/service/src/com/android/server/connectivity/NetworkPermissions.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2025 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 android.net.INetd;
+
+/**
+ * A wrapper class for managing network and traffic permissions.
+ *
+ * This class encapsulates permissions represented as a bitmask, as defined in INetd.aidl
+ * and used within PermissionMonitor.java. It distinguishes between two types of permissions:
+ *
+ * 1. Network Permissions: These permissions, declared in INetd.aidl, are used
+ * by the Android platform's network daemon (system/netd) to control network
+ * management
+ *
+ * 2. Traffic Permissions: These permissions are used internally by PermissionMonitor.java and
+ * BpfNetMaps.java to manage fine-grained network traffic filtering and control.
+ *
+ * This wrapper ensures that no new permission definitions, here or in aidl, conflict with any
+ * existing permissions. This prevents unintended interactions or overrides.
+ *
+ * @hide
+ */
+public class NetworkPermissions {
+
+ /*
+ * Below are network permissions declared in INetd.aidl and used by the platform. Using these is
+ * equivalent to using the values in android.net.INetd.
+ */
+ public static final int PERMISSION_NONE = INetd.PERMISSION_NONE; /* 0 */
+ public static final int PERMISSION_NETWORK = INetd.PERMISSION_NETWORK; /* 1 */
+ public static final int PERMISSION_SYSTEM = INetd.PERMISSION_SYSTEM; /* 2 */
+
+ /*
+ * Below are traffic permissions used by PermissionMonitor and BpfNetMaps.
+ */
+
+ /**
+ * PERMISSION_UNINSTALLED is used when an app is uninstalled from the device. All internet
+ * related permissions need to be cleaned.
+ */
+ public static final int TRAFFIC_PERMISSION_UNINSTALLED = -1;
+
+ /**
+ * PERMISSION_INTERNET indicates that the app can create AF_INET and AF_INET6 sockets.
+ */
+ public static final int TRAFFIC_PERMISSION_INTERNET = 4;
+
+ /**
+ * PERMISSION_UPDATE_DEVICE_STATS is used for system UIDs and privileged apps
+ * that have the UPDATE_DEVICE_STATS permission.
+ */
+ public static final int TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS = 8;
+
+ /**
+ * TRAFFIC_PERMISSION_SDKSANDBOX_LOCALHOST indicates if an SdkSandbox UID will be allowed
+ * to connect to localhost. For non SdkSandbox UIDs this bit is a no-op.
+ */
+ public static final int TRAFFIC_PERMISSION_SDKSANDBOX_LOCALHOST = 16;
+}
diff --git a/service/src/com/android/server/connectivity/PermissionMonitor.java b/service/src/com/android/server/connectivity/PermissionMonitor.java
index 737e27a..5de5f61 100755
--- a/service/src/com/android/server/connectivity/PermissionMonitor.java
+++ b/service/src/com/android/server/connectivity/PermissionMonitor.java
@@ -19,27 +19,29 @@
import static android.Manifest.permission.CHANGE_NETWORK_STATE;
import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
import static android.Manifest.permission.INTERNET;
+import static android.Manifest.permission.NEARBY_WIFI_DEVICES;
import static android.Manifest.permission.NETWORK_STACK;
import static android.Manifest.permission.UPDATE_DEVICE_STATS;
import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED;
import static android.content.pm.PackageManager.GET_PERMISSIONS;
import static android.net.ConnectivitySettingsManager.UIDS_ALLOWED_ON_RESTRICTED_NETWORKS;
-import static android.net.INetd.PERMISSION_INTERNET;
-import static android.net.INetd.PERMISSION_NETWORK;
-import static android.net.INetd.PERMISSION_NONE;
-import static android.net.INetd.PERMISSION_SYSTEM;
-import static android.net.INetd.PERMISSION_UNINSTALLED;
-import static android.net.INetd.PERMISSION_UPDATE_DEVICE_STATS;
import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
import static android.net.connectivity.ConnectivityCompatChanges.RESTRICT_LOCAL_NETWORK;
import static android.os.Process.INVALID_UID;
import static android.os.Process.SYSTEM_UID;
+import static com.android.server.connectivity.NetworkPermissions.PERMISSION_NETWORK;
+import static com.android.server.connectivity.NetworkPermissions.PERMISSION_NONE;
+import static com.android.server.connectivity.NetworkPermissions.PERMISSION_SYSTEM;
+import static com.android.server.connectivity.NetworkPermissions.TRAFFIC_PERMISSION_INTERNET;
+import static com.android.server.connectivity.NetworkPermissions.TRAFFIC_PERMISSION_UNINSTALLED;
+import static com.android.server.connectivity.NetworkPermissions.TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS;
import static com.android.net.module.util.CollectionUtils.toIntArray;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.compat.CompatChanges;
+import android.content.AttributionSource;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -62,6 +64,7 @@
import android.os.SystemConfigManager;
import android.os.UserHandle;
import android.os.UserManager;
+import android.permission.PermissionManager;
import android.provider.Settings;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -72,7 +75,6 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import com.android.modules.utils.build.SdkLevel;
-import com.android.net.flags.Flags;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.SharedLog;
import com.android.networkstack.apishim.ProcessShimImpl;
@@ -99,6 +101,8 @@
private final PackageManager mPackageManager;
private final UserManager mUserManager;
private final SystemConfigManager mSystemConfigManager;
+ private final PermissionManager mPermissionManager;
+ private final PermissionChangeListener mPermissionChangeListener;
private final INetd mNetd;
private final Dependencies mDeps;
private final Context mContext;
@@ -230,6 +234,12 @@
context.getContentResolver().registerContentObserver(
uri, notifyForDescendants, observer);
}
+
+ public boolean shouldEnforceLocalNetRestrictions(int uid) {
+ // TODO(b/394567896): Update compat change checks for enforcement
+ return BpfNetMaps.isAtLeast25Q2() &&
+ CompatChanges.isChangeEnabled(RESTRICT_LOCAL_NETWORK, uid);
+ }
}
private static class MultiSet<T> {
@@ -270,13 +280,15 @@
}
@VisibleForTesting
- PermissionMonitor(@NonNull final Context context, @NonNull final INetd netd,
+ public PermissionMonitor(@NonNull final Context context, @NonNull final INetd netd,
@NonNull final BpfNetMaps bpfNetMaps,
@NonNull final Dependencies deps,
@NonNull final HandlerThread thread) {
mPackageManager = context.getPackageManager();
mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
mSystemConfigManager = context.getSystemService(SystemConfigManager.class);
+ mPermissionManager = context.getSystemService(PermissionManager.class);
+ mPermissionChangeListener = new PermissionChangeListener();
mNetd = netd;
mDeps = deps;
mContext = context;
@@ -287,7 +299,29 @@
// This listener should finish registration by the time the system has completed
// boot setup such that any changes to runtime permissions for local network
// restrictions can only occur after this registration has completed.
- mPackageManager.addOnPermissionsChangeListener(new PermissionChangeListener());
+ mPackageManager.addOnPermissionsChangeListener(mPermissionChangeListener);
+ }
+ }
+
+ @VisibleForTesting
+ void setLocalNetworkPermissions(final int uid, @Nullable final String packageName) {
+ if (!mDeps.shouldEnforceLocalNetRestrictions(uid)) return;
+
+ final AttributionSource attributionSource =
+ new AttributionSource.Builder(uid).setPackageName(packageName).build();
+ final int permissionState = mPermissionManager.checkPermissionForPreflight(
+ NEARBY_WIFI_DEVICES, attributionSource);
+ if (permissionState == PermissionManager.PERMISSION_GRANTED) {
+ mBpfNetMaps.removeUidFromLocalNetBlockMap(attributionSource.getUid());
+ } else {
+ mBpfNetMaps.addUidToLocalNetBlockMap(attributionSource.getUid());
+ }
+ if (hasSdkSandbox(uid)){
+ // SDKs in the SDK RT cannot hold runtime permissions
+ final int sdkSandboxUid = sProcessShim.toSdkSandboxUid(uid);
+ if (!mBpfNetMaps.isUidBlockedFromUsingLocalNetwork(sdkSandboxUid)) {
+ mBpfNetMaps.addUidToLocalNetBlockMap(sdkSandboxUid);
+ }
}
}
@@ -351,6 +385,7 @@
uidsPerm.put(sdkSandboxUid, permission);
}
}
+ setLocalNetworkPermissions(uid, app.packageName);
}
return uidsPerm;
}
@@ -405,7 +440,7 @@
final SparseIntArray appIdsPerm = new SparseIntArray();
for (final int uid : mSystemConfigManager.getSystemPermissionUids(INTERNET)) {
final int appId = UserHandle.getAppId(uid);
- final int permission = appIdsPerm.get(appId) | PERMISSION_INTERNET;
+ final int permission = appIdsPerm.get(appId) | TRAFFIC_PERMISSION_INTERNET;
appIdsPerm.put(appId, permission);
if (hasSdkSandbox(appId)) {
appIdsPerm.put(sProcessShim.toSdkSandboxUid(appId), permission);
@@ -413,7 +448,7 @@
}
for (final int uid : mSystemConfigManager.getSystemPermissionUids(UPDATE_DEVICE_STATS)) {
final int appId = UserHandle.getAppId(uid);
- final int permission = appIdsPerm.get(appId) | PERMISSION_UPDATE_DEVICE_STATS;
+ final int permission = appIdsPerm.get(appId) | TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS;
appIdsPerm.put(appId, permission);
if (hasSdkSandbox(appId)) {
appIdsPerm.put(sProcessShim.toSdkSandboxUid(appId), permission);
@@ -598,7 +633,7 @@
final List<PackageInfo> apps = getInstalledPackagesAsUser(user);
- // Save all apps
+ // Save all apps in mAllApps
updateAllApps(apps);
// Uids network permissions
@@ -635,6 +670,11 @@
final int uid = allUids.keyAt(i);
if (user.equals(UserHandle.getUserHandleForUid(uid))) {
mUidToNetworkPerm.delete(uid);
+ if (mDeps.shouldEnforceLocalNetRestrictions(uid)) {
+ mBpfNetMaps.removeUidFromLocalNetBlockMap(uid);
+ if (hasSdkSandbox(uid)) mBpfNetMaps.removeUidFromLocalNetBlockMap(
+ sProcessShim.toSdkSandboxUid(uid));
+ }
removedUids.put(uid, allUids.valueAt(i));
}
}
@@ -656,7 +696,7 @@
final int appId = removedUserAppIds.keyAt(i);
// Need to clear permission if the removed appId is not found in the array.
if (appIds.indexOfKey(appId) < 0) {
- appIds.put(appId, PERMISSION_UNINSTALLED);
+ appIds.put(appId, TRAFFIC_PERMISSION_UNINSTALLED);
}
}
sendAppIdsTrafficPermission(appIds);
@@ -708,7 +748,7 @@
}
} else {
// The last package of this uid is removed from device. Clean the package up.
- permission = PERMISSION_UNINSTALLED;
+ permission = TRAFFIC_PERMISSION_UNINSTALLED;
}
return permission;
}
@@ -751,13 +791,13 @@
return "NETWORK";
case PERMISSION_SYSTEM:
return "SYSTEM";
- case PERMISSION_INTERNET:
+ case TRAFFIC_PERMISSION_INTERNET:
return "INTERNET";
- case PERMISSION_UPDATE_DEVICE_STATS:
+ case TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS:
return "UPDATE_DEVICE_STATS";
- case (PERMISSION_INTERNET | PERMISSION_UPDATE_DEVICE_STATS):
+ case (TRAFFIC_PERMISSION_INTERNET | TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS):
return "ALL";
- case PERMISSION_UNINSTALLED:
+ case TRAFFIC_PERMISSION_UNINSTALLED:
return "UNINSTALLED";
default:
return "UNKNOWN";
@@ -776,7 +816,7 @@
// (PERMISSION_UNINSTALLED), remove the appId from the array. Otherwise, update the latest
// permission to the appId.
final int appId = UserHandle.getAppId(uid);
- if (uidTrafficPerm == PERMISSION_UNINSTALLED) {
+ if (uidTrafficPerm == TRAFFIC_PERMISSION_UNINSTALLED) {
userTrafficPerms.delete(appId);
} else {
userTrafficPerms.put(appId, uidTrafficPerm);
@@ -794,7 +834,7 @@
installed = true;
}
}
- return installed ? permission : PERMISSION_UNINSTALLED;
+ return installed ? permission : TRAFFIC_PERMISSION_UNINSTALLED;
}
/**
@@ -829,6 +869,7 @@
}
sendUidsNetworkPermission(apps, true /* add */);
}
+ setLocalNetworkPermissions(uid, packageName);
// If the newly-installed package falls within some VPN's uid range, update Netd with it.
// This needs to happen after the mUidToNetworkPerm update above, since
@@ -873,6 +914,11 @@
synchronized void onPackageRemoved(@NonNull final String packageName, final int uid) {
// Update uid permission.
updateAppIdTrafficPermission(uid);
+ if (BpfNetMaps.isAtLeast25Q2()) {
+ mBpfNetMaps.removeUidFromLocalNetBlockMap(uid);
+ if (hasSdkSandbox(uid)) mBpfNetMaps.removeUidFromLocalNetBlockMap(
+ sProcessShim.toSdkSandboxUid(uid));
+ }
// Get the appId permission from all users then send the latest permission to netd.
final int appId = UserHandle.getAppId(uid);
final int appIdTrafficPerm = getAppIdTrafficPermission(appId);
@@ -931,11 +977,11 @@
for (int i = 0; i < requestedPermissions.length; i++) {
if (requestedPermissions[i].equals(INTERNET)
&& ((requestedPermissionsFlags[i] & REQUESTED_PERMISSION_GRANTED) != 0)) {
- permissions |= PERMISSION_INTERNET;
+ permissions |= TRAFFIC_PERMISSION_INTERNET;
}
if (requestedPermissions[i].equals(UPDATE_DEVICE_STATS)
&& ((requestedPermissionsFlags[i] & REQUESTED_PERMISSION_GRANTED) != 0)) {
- permissions |= PERMISSION_UPDATE_DEVICE_STATS;
+ permissions |= TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS;
}
}
return permissions;
@@ -1174,19 +1220,19 @@
for (int i = 0; i < netdPermissionsAppIds.size(); i++) {
int permissions = netdPermissionsAppIds.valueAt(i);
switch(permissions) {
- case (PERMISSION_INTERNET | PERMISSION_UPDATE_DEVICE_STATS):
+ case (TRAFFIC_PERMISSION_INTERNET | TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS):
allPermissionAppIds.add(netdPermissionsAppIds.keyAt(i));
break;
- case PERMISSION_INTERNET:
+ case TRAFFIC_PERMISSION_INTERNET:
internetPermissionAppIds.add(netdPermissionsAppIds.keyAt(i));
break;
- case PERMISSION_UPDATE_DEVICE_STATS:
+ case TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS:
updateStatsPermissionAppIds.add(netdPermissionsAppIds.keyAt(i));
break;
case PERMISSION_NONE:
noPermissionAppIds.add(netdPermissionsAppIds.keyAt(i));
break;
- case PERMISSION_UNINSTALLED:
+ case TRAFFIC_PERMISSION_UNINSTALLED:
uninstalledAppIds.add(netdPermissionsAppIds.keyAt(i));
break;
default:
@@ -1198,15 +1244,15 @@
// TODO: add a lock inside netd to protect IPC trafficSetNetPermForUids()
if (allPermissionAppIds.size() != 0) {
mBpfNetMaps.setNetPermForUids(
- PERMISSION_INTERNET | PERMISSION_UPDATE_DEVICE_STATS,
+ TRAFFIC_PERMISSION_INTERNET | TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS,
toIntArray(allPermissionAppIds));
}
if (internetPermissionAppIds.size() != 0) {
- mBpfNetMaps.setNetPermForUids(PERMISSION_INTERNET,
+ mBpfNetMaps.setNetPermForUids(TRAFFIC_PERMISSION_INTERNET,
toIntArray(internetPermissionAppIds));
}
if (updateStatsPermissionAppIds.size() != 0) {
- mBpfNetMaps.setNetPermForUids(PERMISSION_UPDATE_DEVICE_STATS,
+ mBpfNetMaps.setNetPermForUids(TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS,
toIntArray(updateStatsPermissionAppIds));
}
if (noPermissionAppIds.size() != 0) {
@@ -1214,7 +1260,7 @@
toIntArray(noPermissionAppIds));
}
if (uninstalledAppIds.size() != 0) {
- mBpfNetMaps.setNetPermForUids(PERMISSION_UNINSTALLED,
+ mBpfNetMaps.setNetPermForUids(TRAFFIC_PERMISSION_UNINSTALLED,
toIntArray(uninstalledAppIds));
}
} catch (RemoteException | ServiceSpecificException e) {
@@ -1325,16 +1371,7 @@
private class PermissionChangeListener implements PackageManager.OnPermissionsChangedListener {
@Override
public void onPermissionsChanged(int uid) {
- // RESTRICT_LOCAL_NETWORK is a compat change that is enabled when developers manually
- // opt-in to this change, or when the app's targetSdkVersion is greater than 36.
- // The RESTRICT_LOCAL_NETWORK compat change is used here instead of the
- // Flags.restrictLocalNetwork() is used to offer the feature to devices, but it will
- // only be enforced when develoeprs choose to enable it.
- // TODO(b/394567896): Update compat change checks
- if (CompatChanges.isChangeEnabled(RESTRICT_LOCAL_NETWORK, uid)
- && BpfNetMaps.isAtLeast25Q2()) {
- // TODO(b/388803658): Update network permissions and record change
- }
+ setLocalNetworkPermissions(uid, null);
}
}
}
diff --git a/service/src/com/android/server/net/HeaderCompressionUtils.java b/service/src/com/android/server/net/HeaderCompressionUtils.java
new file mode 100644
index 0000000..5bd3a76
--- /dev/null
+++ b/service/src/com/android/server/net/HeaderCompressionUtils.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2025 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.net;
+
+import android.util.Log;
+
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+public class HeaderCompressionUtils {
+ private static final String TAG = "L2capHeaderCompressionUtils";
+ private static final int IPV6_HEADER_SIZE = 40;
+
+ private static byte[] decodeIpv6Address(ByteBuffer buffer, int mode, boolean isMulticast)
+ throws BufferUnderflowException, IOException {
+ // Mode is equivalent between SAM and DAM; however, isMulticast only applies to DAM.
+ final byte[] address = new byte[16];
+ // If multicast bit is set, mix it in the mode, so that the lower two bits represent the
+ // address mode, and the upper bit represents multicast compression.
+ switch ((isMulticast ? 0b100 : 0) | mode) {
+ case 0b000: // 128 bits. The full address is carried in-line.
+ case 0b100:
+ buffer.get(address);
+ break;
+ case 0b001: // 64 bits. The first 64-bits of the fe80:: address are elided.
+ address[0] = (byte) 0xfe;
+ address[1] = (byte) 0x80;
+ buffer.get(address, 8 /*off*/, 8 /*len*/);
+ break;
+ case 0b010: // 16 bits. fe80::ff:fe00:XXXX, where XXXX are the bits carried in-line
+ address[0] = (byte) 0xfe;
+ address[1] = (byte) 0x80;
+ address[11] = (byte) 0xff;
+ address[12] = (byte) 0xfe;
+ buffer.get(address, 14 /*off*/, 2 /*len*/);
+ break;
+ case 0b011: // 0 bits. The address is fully elided and derived from BLE MAC address
+ // Note that on Android, the BLE MAC addresses are not exposed via the API;
+ // therefore, this compression mode cannot be supported.
+ throw new IOException("Address cannot be fully elided");
+ case 0b101: // 48 bits. The address takes the form ffXX::00XX:XXXX:XXXX.
+ address[0] = (byte) 0xff;
+ address[1] = buffer.get();
+ buffer.get(address, 11 /*off*/, 5 /*len*/);
+ break;
+ case 0b110: // 32 bits. The address takes the form ffXX::00XX:XXXX
+ address[0] = (byte) 0xff;
+ address[1] = buffer.get();
+ buffer.get(address, 13 /*off*/, 3 /*len*/);
+ break;
+ case 0b111: // 8 bits. The address takes the form ff02::00XX.
+ address[0] = (byte) 0xff;
+ address[1] = (byte) 0x02;
+ address[15] = buffer.get();
+ break;
+ }
+ return address;
+ }
+
+ /**
+ * Performs 6lowpan header decompression in place.
+ *
+ * Note that the passed in buffer must have enough capacity for successful decompression.
+ *
+ * @param bytes The buffer containing the packet.
+ * @param len The size of the packet
+ * @return decompressed size or zero
+ * @throws BufferUnderflowException if an illegal packet is encountered.
+ * @throws IOException if an unsupported option is encountered.
+ */
+ public static int decompress6lowpan(byte[] bytes, int len)
+ throws BufferUnderflowException, IOException {
+ // Note that ByteBuffer's default byte order is big endian.
+ final ByteBuffer inBuffer = ByteBuffer.wrap(bytes);
+ inBuffer.limit(len);
+
+ // LOWPAN_IPHC base encoding:
+ // 0 1 2 3 4 5 6 7 | 8 9 10 11 12 13 14 15
+ // +---+---+---+---+---+---+---+---|---+---+---+---+---+---+---+---+
+ // | 0 | 1 | 1 | TF |NH | HLIM |CID|SAC| SAM | M |DAC| DAM |
+ // +---+---+---+---+---+---+---+---|---+---+---+---+---+---+---+---+
+ final int iphc1 = inBuffer.get() & 0xff;
+ final int iphc2 = inBuffer.get() & 0xff;
+ // Dispatch must start with 0b011.
+ if ((iphc1 & 0xe0) != 0x60) {
+ throw new IOException("LOWPAN_IPHC does not start with 011");
+ }
+
+ final int tf = (iphc1 >> 3) & 3; // Traffic class
+ final boolean nh = (iphc1 & 4) != 0; // Next header
+ final int hlim = iphc1 & 3; // Hop limit
+ final boolean cid = (iphc2 & 0x80) != 0; // Context identifier extension
+ final boolean sac = (iphc2 & 0x40) != 0; // Source address compression
+ final int sam = (iphc2 >> 4) & 3; // Source address mode
+ final boolean m = (iphc2 & 8) != 0; // Multicast compression
+ final boolean dac = (iphc2 & 4) != 0; // Destination address compression
+ final int dam = iphc2 & 3; // Destination address mode
+
+ final ByteBuffer ipv6Header = ByteBuffer.allocate(IPV6_HEADER_SIZE);
+
+ final int trafficClass;
+ final int flowLabel;
+ switch (tf) {
+ case 0b00: // ECN + DSCP + 4-bit Pad + Flow Label (4 bytes)
+ trafficClass = inBuffer.get() & 0xff;
+ flowLabel = (inBuffer.get() & 0x0f) << 16
+ | (inBuffer.get() & 0xff) << 8
+ | (inBuffer.get() & 0xff);
+ break;
+ case 0b01: // ECN + 2-bit Pad + Flow Label (3 bytes), DSCP is elided.
+ final int firstByte = inBuffer.get() & 0xff;
+ // 0 1 2 3 4 5 6 7
+ // +-----+-----+-----+-----+-----+-----+-----+-----+
+ // | DS FIELD, DSCP | ECN FIELD |
+ // +-----+-----+-----+-----+-----+-----+-----+-----+
+ // rfc6282 does not explicitly state what value to use for DSCP, assuming 0.
+ trafficClass = firstByte >> 6;
+ flowLabel = (firstByte & 0x0f) << 16
+ | (inBuffer.get() & 0xff) << 8
+ | (inBuffer.get() & 0xff);
+ break;
+ case 0b10: // ECN + DSCP (1 byte), Flow Label is elided.
+ trafficClass = inBuffer.get() & 0xff;
+ // rfc6282 does not explicitly state what value to use, assuming 0.
+ flowLabel = 0;
+ break;
+ case 0b11: // Traffic Class and Flow Label are elided.
+ // rfc6282 does not explicitly state what value to use, assuming 0.
+ trafficClass = 0;
+ flowLabel = 0;
+ break;
+ default:
+ // This cannot happen. Crash if it does.
+ throw new IllegalStateException("Illegal TF value");
+ }
+
+ // Write version, traffic class, and flow label
+ final int versionTcFlowLabel = (6 << 28) | (trafficClass << 20) | flowLabel;
+ ipv6Header.putInt(versionTcFlowLabel);
+
+ // Payload length is still unknown. Use 0 for now.
+ ipv6Header.putShort((short) 0);
+
+ // We do not use UDP or extension header compression, therefore the next header
+ // cannot be compressed.
+ if (nh) throw new IOException("Next header cannot be compressed");
+ // Write next header
+ ipv6Header.put(inBuffer.get());
+
+ final byte hopLimit;
+ switch (hlim) {
+ case 0b00: // The Hop Limit field is carried in-line.
+ hopLimit = inBuffer.get();
+ break;
+ case 0b01: // The Hop Limit field is compressed and the hop limit is 1.
+ hopLimit = 1;
+ break;
+ case 0b10: // The Hop Limit field is compressed and the hop limit is 64.
+ hopLimit = 64;
+ break;
+ case 0b11: // The Hop Limit field is compressed and the hop limit is 255.
+ hopLimit = (byte) 255;
+ break;
+ default:
+ // This cannot happen. Crash if it does.
+ throw new IllegalStateException("Illegal HLIM value");
+ }
+ ipv6Header.put(hopLimit);
+
+ if (cid) throw new IOException("Context based compression not supported");
+ if (sac) throw new IOException("Context based compression not supported");
+ if (dac) throw new IOException("Context based compression not supported");
+
+ // Write source address
+ ipv6Header.put(decodeIpv6Address(inBuffer, sam, false /* isMulticast */));
+
+ // Write destination address
+ ipv6Header.put(decodeIpv6Address(inBuffer, dam, m));
+
+ // Go back and fix up payloadLength
+ final short payloadLength = (short) inBuffer.remaining();
+ ipv6Header.putShort(4, payloadLength);
+
+ // Done! Check that 40 bytes were written.
+ if (ipv6Header.position() != IPV6_HEADER_SIZE) {
+ // This indicates a bug in our code -> crash.
+ throw new IllegalStateException("Faulty decompression wrote less than 40 bytes");
+ }
+
+ // Ensure there is enough room in the buffer
+ final int packetLength = payloadLength + IPV6_HEADER_SIZE;
+ if (bytes.length < packetLength) {
+ throw new IOException("Decompressed packet exceeds buffer size");
+ }
+
+ // Move payload bytes back to make room for the header
+ inBuffer.limit(packetLength);
+ System.arraycopy(bytes, inBuffer.position(), bytes, IPV6_HEADER_SIZE, payloadLength);
+ // Copy IPv6 header to the beginning of the buffer.
+ inBuffer.position(0);
+ ipv6Header.flip();
+ inBuffer.put(ipv6Header);
+
+ return packetLength;
+ }
+
+ /**
+ * Performs 6lowpan header compression in place.
+ *
+ * @param bytes The buffer containing the packet.
+ * @param len The size of the packet
+ * @return compressed size or zero
+ * @throws BufferUnderflowException if an illegal packet is encountered.
+ * @throws IOException if an unsupported option is encountered.
+ */
+ public static int compress6lowpan(byte[] bytes, final int len)
+ throws BufferUnderflowException, IOException {
+ // Compression only happens on egress, i.e. the packet is read from the tun fd.
+ // This means that this code can be a bit more lenient.
+ if (len < 40) {
+ Log.wtf(TAG, "Encountered short (<40 byte) packet");
+ return 0;
+ }
+
+ // Note that ByteBuffer's default byte order is big endian.
+ final ByteBuffer inBuffer = ByteBuffer.wrap(bytes);
+ inBuffer.limit(len);
+
+ // Check that the packet is an IPv6 packet
+ final int versionTcFlowLabel = inBuffer.getInt() & 0xffffffff;
+ if ((versionTcFlowLabel >> 28) != 6) {
+ return 0;
+ }
+
+ // Check that the payload length matches the packet length - 40.
+ int payloadLength = inBuffer.getShort();
+ if (payloadLength != len - IPV6_HEADER_SIZE) {
+ throw new IOException("Encountered packet with payload length mismatch");
+ }
+
+ // Implements rfc 6282 6lowpan header compression using iphc 0110 0000 0000 0000 (all
+ // fields are carried inline).
+ inBuffer.position(0);
+ inBuffer.put((byte) 0x60);
+ inBuffer.put((byte) 0x00);
+ final byte trafficClass = (byte) ((versionTcFlowLabel >> 20) & 0xff);
+ inBuffer.put(trafficClass);
+ final byte flowLabelMsb = (byte) ((versionTcFlowLabel >> 16) & 0x0f);
+ final short flowLabelLsb = (short) (versionTcFlowLabel & 0xffff);
+ inBuffer.put(flowLabelMsb);
+ // Note: the next putShort overrides the payload length. This is WAI as the payload length
+ // is reconstructed via L2CAP packet length.
+ inBuffer.putShort(flowLabelLsb);
+
+ // Since the iphc (2 bytes) matches the payload length that was elided (2 bytes), the length
+ // of the packet did not change.
+ return len;
+ }
+}
diff --git a/service/src/com/android/server/net/L2capNetwork.java b/service/src/com/android/server/net/L2capNetwork.java
index b9d5f13..c7417f9 100644
--- a/service/src/com/android/server/net/L2capNetwork.java
+++ b/service/src/com/android/server/net/L2capNetwork.java
@@ -16,9 +16,12 @@
package com.android.server.net;
+import static android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_6LOWPAN;
+
import android.annotation.Nullable;
import android.bluetooth.BluetoothSocket;
import android.content.Context;
+import android.net.L2capNetworkSpecifier;
import android.net.LinkProperties;
import android.net.NetworkAgent;
import android.net.NetworkAgentConfig;
@@ -35,14 +38,14 @@
import android.os.ParcelFileDescriptor;
import android.util.Log;
+import com.android.server.L2capNetworkProvider;
+
public class L2capNetwork {
private static final NetworkScore NETWORK_SCORE = new NetworkScore.Builder().build();
private final String mLogTag;
private final Handler mHandler;
- private final String mIfname;
private final L2capPacketForwarder mForwarder;
private final NetworkCapabilities mNetworkCapabilities;
- private final L2capIpClient mIpClient;
private final NetworkAgent mNetworkAgent;
/** IpClient wrapper to handle IPv6 link-local provisioning for L2CAP tun.
@@ -56,7 +59,7 @@
@Nullable
private IpClientManager mIpClient;
@Nullable
- private LinkProperties mLinkProperties;
+ private volatile LinkProperties mLinkProperties;
L2capIpClient(String logTag, Context context, String ifname) {
mLogTag = logTag;
@@ -71,11 +74,24 @@
@Override
public void onProvisioningSuccess(LinkProperties lp) {
- Log.d(mLogTag, "Successfully provisionined l2cap tun: " + lp);
+ Log.d(mLogTag, "Successfully provisioned l2cap tun: " + lp);
mLinkProperties = lp;
mOnProvisioningSuccessCv.open();
}
+ @Override
+ public void onProvisioningFailure(LinkProperties lp) {
+ Log.i(mLogTag, "Failed to provision l2cap tun: " + lp);
+ mLinkProperties = null;
+ mOnProvisioningSuccessCv.open();
+ }
+
+ /**
+ * Starts IPv6 link-local provisioning.
+ *
+ * @return LinkProperties on success, null on failure.
+ */
+ @Nullable
public LinkProperties start() {
mOnIpClientCreatedCv.block();
// mIpClient guaranteed non-null.
@@ -99,27 +115,28 @@
void onNetworkUnwanted(L2capNetwork network);
}
- public L2capNetwork(Handler handler, Context context, NetworkProvider provider, String ifname,
- BluetoothSocket socket, ParcelFileDescriptor tunFd,
- NetworkCapabilities networkCapabilities, ICallback cb) {
- // TODO: add a check that this constructor is invoked on the handler thread.
- mLogTag = String.format("L2capNetwork[%s]", ifname);
+ public L2capNetwork(String logTag, Handler handler, Context context, NetworkProvider provider,
+ BluetoothSocket socket, ParcelFileDescriptor tunFd, NetworkCapabilities nc,
+ LinkProperties lp, L2capNetworkProvider.Dependencies deps, ICallback cb) {
+ mLogTag = logTag;
mHandler = handler;
- mIfname = ifname;
- mForwarder = new L2capPacketForwarder(handler, tunFd, socket, () -> {
+ mNetworkCapabilities = nc;
+
+ final L2capNetworkSpecifier spec = (L2capNetworkSpecifier) nc.getNetworkSpecifier();
+ final boolean compressHeaders = spec.getHeaderCompression() == HEADER_COMPRESSION_6LOWPAN;
+
+ mForwarder = deps.createL2capPacketForwarder(handler, tunFd, socket, compressHeaders,
+ () -> {
// TODO: add a check that this callback is invoked on the handler thread.
cb.onError(L2capNetwork.this);
});
- mNetworkCapabilities = networkCapabilities;
- mIpClient = new L2capIpClient(mLogTag, context, ifname);
- final LinkProperties linkProperties = mIpClient.start();
final NetworkAgentConfig config = new NetworkAgentConfig.Builder().build();
mNetworkAgent = new NetworkAgent(context, mHandler.getLooper(), mLogTag,
- networkCapabilities, linkProperties, NETWORK_SCORE, config, provider) {
+ nc, lp, NETWORK_SCORE, config, provider) {
@Override
public void onNetworkUnwanted() {
- Log.i(mLogTag, mIfname + ": Network is unwanted");
+ Log.i(mLogTag, "Network is unwanted");
// TODO: add a check that this callback is invoked on the handler thread.
cb.onNetworkUnwanted(L2capNetwork.this);
}
@@ -128,6 +145,25 @@
mNetworkAgent.markConnected();
}
+ /** Create an L2capNetwork or return null on failure. */
+ @Nullable
+ public static L2capNetwork create(Handler handler, Context context, NetworkProvider provider,
+ String ifname, BluetoothSocket socket, ParcelFileDescriptor tunFd,
+ NetworkCapabilities nc, L2capNetworkProvider.Dependencies deps, ICallback cb) {
+ // TODO: add a check that this function is invoked on the handler thread.
+ final String logTag = String.format("L2capNetwork[%s]", ifname);
+
+ // L2capIpClient#start() blocks until provisioning either succeeds (and returns
+ // LinkProperties) or fails (and returns null).
+ // Note that since L2capNetwork is using IPv6 link-local provisioning the most likely
+ // (only?) failure mode is due to the interface disappearing.
+ final LinkProperties lp = new L2capIpClient(logTag, context, ifname).start();
+ if (lp == null) return null;
+
+ return new L2capNetwork(
+ logTag, handler, context, provider, socket, tunFd, nc, lp, deps, cb);
+ }
+
/** Get the NetworkCapabilities used for this Network */
public NetworkCapabilities getNetworkCapabilities() {
return mNetworkCapabilities;
diff --git a/service/src/com/android/server/net/L2capPacketForwarder.java b/service/src/com/android/server/net/L2capPacketForwarder.java
index cef351c..737cb9c 100644
--- a/service/src/com/android/server/net/L2capPacketForwarder.java
+++ b/service/src/com/android/server/net/L2capPacketForwarder.java
@@ -16,6 +16,9 @@
package com.android.server.net;
+import static com.android.server.net.HeaderCompressionUtils.compress6lowpan;
+import static com.android.server.net.HeaderCompressionUtils.decompress6lowpan;
+
import android.bluetooth.BluetoothSocket;
import android.os.Handler;
import android.os.ParcelFileDescriptor;
@@ -29,6 +32,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.nio.BufferUnderflowException;
/**
* Forwards packets from a BluetoothSocket of type L2CAP to a tun fd and vice versa.
@@ -222,13 +226,19 @@
private volatile boolean mIsRunning = true;
private final String mLogTag;
- private IReadWriteFd mReadFd;
- private IReadWriteFd mWriteFd;
+ private final IReadWriteFd mReadFd;
+ private final IReadWriteFd mWriteFd;
+ private final boolean mIsIngress;
+ private final boolean mCompressHeaders;
- L2capThread(String logTag, IReadWriteFd readFd, IReadWriteFd writeFd) {
- mLogTag = logTag;
+ L2capThread(IReadWriteFd readFd, IReadWriteFd writeFd, boolean isIngress,
+ boolean compressHeaders) {
+ super("L2capNetworkProvider-ForwarderThread");
+ mLogTag = isIngress ? "L2capForwarderThread-Ingress" : "L2capForwarderThread-Egress";
mReadFd = readFd;
mWriteFd = writeFd;
+ mIsIngress = isIngress;
+ mCompressHeaders = compressHeaders;
}
private void postOnError() {
@@ -242,20 +252,28 @@
public void run() {
while (mIsRunning) {
try {
- final int readBytes = mReadFd.read(mBuffer, 0 /*off*/, mBuffer.length);
+ int readBytes = mReadFd.read(mBuffer, 0 /*off*/, mBuffer.length);
// No bytes to write, continue.
if (readBytes <= 0) {
Log.w(mLogTag, "Zero-byte read encountered: " + readBytes);
continue;
}
- // If the packet exceeds MTU, drop it.
+ if (mCompressHeaders) {
+ if (mIsIngress) {
+ readBytes = decompress6lowpan(mBuffer, readBytes);
+ } else {
+ readBytes = compress6lowpan(mBuffer, readBytes);
+ }
+ }
+
+ // If the packet is 0-length post de/compression or exceeds MTU, drop it.
// Note that a large read on BluetoothSocket throws an IOException to tear down
// the network.
- if (readBytes > MTU) continue;
+ if (readBytes <= 0 || readBytes > MTU) continue;
mWriteFd.write(mBuffer, 0 /*off*/, readBytes);
- } catch (IOException e) {
+ } catch (IOException|BufferUnderflowException e) {
Log.e(mLogTag, "L2capThread exception", e);
// Tear down the network on any error.
mIsRunning = false;
@@ -273,19 +291,20 @@
}
public L2capPacketForwarder(Handler handler, ParcelFileDescriptor tunFd, BluetoothSocket socket,
- ICallback cb) {
- this(handler, new FdWrapper(tunFd), new BluetoothSocketWrapper(socket), cb);
+ boolean compressHdrs, ICallback cb) {
+ this(handler, new FdWrapper(tunFd), new BluetoothSocketWrapper(socket), compressHdrs, cb);
}
@VisibleForTesting
- L2capPacketForwarder(Handler handler, IReadWriteFd tunFd, IReadWriteFd l2capFd, ICallback cb) {
+ L2capPacketForwarder(Handler handler, IReadWriteFd tunFd, IReadWriteFd l2capFd,
+ boolean compressHeaders, ICallback cb) {
mHandler = handler;
mTunFd = tunFd;
mL2capFd = l2capFd;
mCallback = cb;
- mIngressThread = new L2capThread("L2capThread-Ingress", l2capFd, tunFd);
- mEgressThread = new L2capThread("L2capThread-Egress", tunFd, l2capFd);
+ mIngressThread = new L2capThread(l2capFd, tunFd, true /*isIngress*/, compressHeaders);
+ mEgressThread = new L2capThread(tunFd, l2capFd, false /*isIngress*/, compressHeaders);
mIngressThread.start();
mEgressThread.start();
diff --git a/staticlibs/device/com/android/net/module/util/SkDestroyListener.java b/staticlibs/device/com/android/net/module/util/SkDestroyListener.java
new file mode 100644
index 0000000..c7c2829
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/SkDestroyListener.java
@@ -0,0 +1,113 @@
+/*
+ * 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 com.android.net.module.util;
+
+import static android.system.OsConstants.NETLINK_INET_DIAG;
+
+import android.os.Handler;
+
+import com.android.net.module.util.ip.NetlinkMonitor;
+import com.android.net.module.util.netlink.InetDiagMessage;
+import com.android.net.module.util.netlink.NetlinkMessage;
+
+import java.io.PrintWriter;
+import java.util.function.Consumer;
+
+/**
+ * Monitor socket destroy and delete entry from cookie tag bpf map.
+ */
+public class SkDestroyListener extends NetlinkMonitor {
+ private static final int SKNLGRP_INET_TCP_DESTROY = 1;
+ private static final int SKNLGRP_INET_UDP_DESTROY = 2;
+ private static final int SKNLGRP_INET6_TCP_DESTROY = 3;
+ private static final int SKNLGRP_INET6_UDP_DESTROY = 4;
+
+ // TODO: if too many sockets are closed too quickly, this can overflow the socket buffer, and
+ // some entries in mCookieTagMap will not be freed. In order to fix this it would be needed to
+ // periodically dump all sockets and remove the tag entries for sockets that have been closed.
+ // For now, set a large-enough buffer that hundreds of sockets can be closed without getting
+ // ENOBUFS and leaking mCookieTagMap entries.
+ private static final int SOCK_RCV_BUF_SIZE = 512 * 1024;
+
+ private final Consumer<InetDiagMessage> mSkDestroyCallback;
+
+ /**
+ * Return SkDestroyListener that monitor both TCP and UDP socket destroy
+ *
+ * @param consumer The consumer that processes InetDiagMessage
+ * @param handler The Handler on which to poll for messages
+ * @param log A SharedLog to log to.
+ * @return SkDestroyListener
+ */
+ public static SkDestroyListener makeSkDestroyListener(final Consumer<InetDiagMessage> consumer,
+ final Handler handler, final SharedLog log) {
+ return makeSkDestroyListener(consumer, true /* monitorTcpSocket */,
+ true /* monitorUdpSocket */, handler, log);
+ }
+
+ /**
+ * Return SkDestroyListener that monitor socket destroy
+ *
+ * @param consumer The consumer that processes InetDiagMessage
+ * @param monitorTcpSocket {@code true} to monitor TCP socket destroy
+ * @param monitorUdpSocket {@code true} to monitor UDP socket destroy
+ * @param handler The Handler on which to poll for messages
+ * @param log A SharedLog to log to.
+ * @return SkDestroyListener
+ */
+ public static SkDestroyListener makeSkDestroyListener(final Consumer<InetDiagMessage> consumer,
+ final boolean monitorTcpSocket, final boolean monitorUdpSocket,
+ final Handler handler, final SharedLog log) {
+ if (!monitorTcpSocket && !monitorUdpSocket) {
+ throw new IllegalArgumentException(
+ "Both monitorTcpSocket and monitorUdpSocket can not be false");
+ }
+ int bindGroups = 0;
+ if (monitorTcpSocket) {
+ bindGroups |= 1 << (SKNLGRP_INET_TCP_DESTROY - 1)
+ | 1 << (SKNLGRP_INET6_TCP_DESTROY - 1);
+ }
+ if (monitorUdpSocket) {
+ bindGroups |= 1 << (SKNLGRP_INET_UDP_DESTROY - 1)
+ | 1 << (SKNLGRP_INET6_UDP_DESTROY - 1);
+ }
+ return new SkDestroyListener(consumer, bindGroups, handler, log);
+ }
+
+ private SkDestroyListener(final Consumer<InetDiagMessage> consumer, final int bindGroups,
+ final Handler handler, final SharedLog log) {
+ super(handler, log, "SkDestroyListener", NETLINK_INET_DIAG,
+ bindGroups, SOCK_RCV_BUF_SIZE);
+ mSkDestroyCallback = consumer;
+ }
+
+ @Override
+ public void processNetlinkMessage(final NetlinkMessage nlMsg, final long whenMs) {
+ if (!(nlMsg instanceof InetDiagMessage)) {
+ mLog.e("Received non InetDiagMessage");
+ return;
+ }
+ mSkDestroyCallback.accept((InetDiagMessage) nlMsg);
+ }
+
+ /**
+ * Dump the contents of SkDestroyListener log.
+ */
+ public void dump(PrintWriter pw) {
+ mLog.reverseDump(pw);
+ }
+}
diff --git a/tests/unit/java/com/android/server/net/SkDestroyListenerTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/SkDestroyListenerTest.kt
similarity index 89%
rename from tests/unit/java/com/android/server/net/SkDestroyListenerTest.kt
rename to staticlibs/tests/unit/src/com/android/net/module/util/SkDestroyListenerTest.kt
index 18785e5..e4b47fe 100644
--- a/tests/unit/java/com/android/server/net/SkDestroyListenerTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/SkDestroyListenerTest.kt
@@ -14,11 +14,11 @@
* limitations under the License.
*/
-package com.android.server.net
+package com.android.net.module.util
import android.os.Handler
import android.os.HandlerThread
-import com.android.net.module.util.SharedLog
+import com.android.net.module.util.SkDestroyListener.makeSkDestroyListener
import com.android.testutils.DevSdkIgnoreRunner
import java.io.PrintWriter
import org.junit.After
@@ -54,7 +54,7 @@
doReturn(sharedLog).`when`(sharedLog).forSubComponent(any())
val handler = Handler(handlerThread.looper)
- val skDestroylistener = SkDestroyListener(null /* cookieTagMap */, handler, sharedLog)
+ val skDestroylistener = makeSkDestroyListener({} /* consumer */, handler, sharedLog)
val pw = PrintWriter(System.out)
skDestroylistener.dump(pw)
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/SetFeatureFlagsRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/SetFeatureFlagsRule.kt
index 7b970d3..0b239b4 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/SetFeatureFlagsRule.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/SetFeatureFlagsRule.kt
@@ -68,10 +68,15 @@
* If any `@FeatureFlag` annotation is found, it passes every feature flag's name
* and enabled state into the user-specified lambda to apply custom actions.
*/
+ private val parameterizedRegexp = Regex("\\[\\d+\\]$")
override fun apply(base: Statement, description: Description): Statement {
return object : Statement() {
override fun evaluate() {
- val testMethod = description.testClass.getMethod(description.methodName)
+ // If the same class also uses Parameterized, depending on evaluation order the
+ // method names here may be synthetic method names, where [0] [1] or so are added
+ // at the end of the method name. Find the original method name.
+ val methodName = description.methodName.replace(parameterizedRegexp, "")
+ val testMethod = description.testClass.getMethod(methodName)
val featureFlagAnnotations = testMethod.getAnnotationsByType(
FeatureFlag::class.java
)
diff --git a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
index ee2e7db..c6a1b09 100644
--- a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
+++ b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
@@ -19,8 +19,6 @@
package android.net.cts
-import android.Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG
-import android.Manifest.permission.WRITE_DEVICE_CONFIG
import android.content.pm.PackageManager.FEATURE_AUTOMOTIVE
import android.content.pm.PackageManager.FEATURE_LEANBACK
import android.content.pm.PackageManager.FEATURE_WIFI
@@ -55,8 +53,6 @@
import android.os.SystemProperties
import android.os.UserManager
import android.platform.test.annotations.AppModeFull
-import android.provider.DeviceConfig
-import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY
import android.system.Os
import android.system.OsConstants
import android.system.OsConstants.AF_INET6
@@ -90,7 +86,6 @@
import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
import com.android.testutils.SkipPresubmit
import com.android.testutils.TestableNetworkCallback
-import com.android.testutils.runAsShell
import com.android.testutils.waitForIdle
import com.google.common.truth.Expect
import com.google.common.truth.Truth.assertThat
@@ -116,7 +111,6 @@
private const val TAG = "ApfIntegrationTest"
private const val TIMEOUT_MS = 2000L
-private const val APF_NEW_RA_FILTER_VERSION = "apf_new_ra_filter_version"
private const val POLLING_INTERVAL_MS: Int = 100
private const val RCV_BUFFER_SIZE = 1480
private const val PING_HEADER_LENGTH = 8
@@ -192,16 +186,6 @@
Thread.sleep(1000)
// TODO: check that there is no active wifi network. Otherwise, ApfFilter has already been
// created.
- // APF adb cmds are only implemented in ApfFilter.java. Enable experiment to prevent
- // LegacyApfFilter.java from being used.
- runAsShell(WRITE_DEVICE_CONFIG, WRITE_ALLOWLISTED_DEVICE_CONFIG) {
- DeviceConfig.setProperty(
- NAMESPACE_CONNECTIVITY,
- APF_NEW_RA_FILTER_VERSION,
- "1", // value => force enabled
- false // makeDefault
- )
- }
}
@AfterClass
@@ -489,15 +473,15 @@
fun ApfV4GeneratorBase<*>.addPassIfNotIcmpv6EchoReply() {
// If not IPv6 -> PASS
- addLoad16(R0, ETH_ETHERTYPE_OFFSET)
+ addLoad16intoR0(ETH_ETHERTYPE_OFFSET)
addJumpIfR0NotEquals(ETH_P_IPV6.toLong(), BaseApfGenerator.PASS_LABEL)
// If not ICMPv6 -> PASS
- addLoad8(R0, IPV6_NEXT_HEADER_OFFSET)
+ addLoad8intoR0(IPV6_NEXT_HEADER_OFFSET)
addJumpIfR0NotEquals(IPPROTO_ICMPV6.toLong(), BaseApfGenerator.PASS_LABEL)
// If not echo reply -> PASS
- addLoad8(R0, ICMP6_TYPE_OFFSET)
+ addLoad8intoR0(ICMP6_TYPE_OFFSET)
addJumpIfR0NotEquals(0x81, BaseApfGenerator.PASS_LABEL)
}
@@ -744,11 +728,11 @@
// transmit 3 ICMPv6 echo requests with random first byte
// increase DROPPED_IPV6_NS_REPLIED_NON_DAD counter
// drop
- gen.addLoad16(R0, ETH_ETHERTYPE_OFFSET)
+ gen.addLoad16intoR0(ETH_ETHERTYPE_OFFSET)
.addJumpIfR0NotEquals(ETH_P_IPV6.toLong(), skipPacketLabel)
- .addLoad8(R0, IPV6_NEXT_HEADER_OFFSET)
+ .addLoad8intoR0(IPV6_NEXT_HEADER_OFFSET)
.addJumpIfR0NotEquals(IPPROTO_ICMPV6.toLong(), skipPacketLabel)
- .addLoad8(R0, ICMP6_TYPE_OFFSET)
+ .addLoad8intoR0(ICMP6_TYPE_OFFSET)
.addJumpIfR0NotEquals(ICMP6_ECHO_REPLY.toLong(), skipPacketLabel)
.addLoadFromMemory(R0, MemorySlot.PACKET_SIZE)
.addCountAndPassIfR0Equals(
diff --git a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
index 1de4cf9..ceccf0b 100644
--- a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
+++ b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
@@ -44,6 +44,7 @@
import android.net.RouteInfo
import android.net.TestNetworkInterface
import android.net.TestNetworkManager
+import android.net.TestNetworkManager.TestInterfaceRequest
import android.net.cts.util.CtsNetUtils.TestNetworkCallback
import android.os.HandlerThread
import android.os.SystemClock
@@ -164,7 +165,11 @@
// Only statically configure the IPv4 address; for IPv6, use the SLAAC generated
// address.
- iface = tnm.createTapInterface(arrayOf(LinkAddress(LOCAL_IPV4_ADDRESS, IP4_PREFIX_LEN)))
+ val req = TestInterfaceRequest.Builder()
+ .setTap()
+ .addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, IP4_PREFIX_LEN))
+ .build()
+ iface = tnm.createTestInterface(req)
assertNotNull(iface)
}
diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
index 06f2075..9f32132 100644
--- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
@@ -51,6 +51,7 @@
import android.net.StaticIpConfiguration
import android.net.TestNetworkInterface
import android.net.TestNetworkManager
+import android.net.TestNetworkManager.TestInterfaceRequest
import android.net.cts.EthernetManagerTest.EthernetStateListener.CallbackEntry.EthernetStateChanged
import android.net.cts.EthernetManagerTest.EthernetStateListener.CallbackEntry.InterfaceStateChanged
import android.os.Build
@@ -169,7 +170,12 @@
// false, it is subsequently disabled. This means that the interface may briefly get
// link. With IPv6 provisioning delays (RS delay and DAD) disabled, this can cause
// tests that expect no network to come up when hasCarrier is false to become flaky.
- tnm.createTapInterface(hasCarrier, false /* bringUp */)
+ val req = TestInterfaceRequest.Builder()
+ .setTap()
+ .setHasCarrier(hasCarrier)
+ .setBringUp(false)
+ .build()
+ tnm.createTestInterface(req)
}
val mtu = tapInterface.mtu
packetReader = PollPacketReader(
diff --git a/tests/cts/net/src/android/net/cts/NetworkReservationTest.kt b/tests/cts/net/src/android/net/cts/NetworkReservationTest.kt
index f05bf15..a9af34f 100644
--- a/tests/cts/net/src/android/net/cts/NetworkReservationTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkReservationTest.kt
@@ -16,15 +16,21 @@
package android.net.cts
+import android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS
import android.Manifest.permission.MANAGE_TEST_NETWORKS
import android.Manifest.permission.NETWORK_SETTINGS
import android.net.ConnectivityManager
+import android.net.L2capNetworkSpecifier
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_6LOWPAN
+import android.net.L2capNetworkSpecifier.ROLE_SERVER
import android.net.NetworkCapabilities
import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED
import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
import android.net.NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS
+import android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH
import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
import android.net.NetworkCapabilities.TRANSPORT_TEST
import android.net.NetworkProvider
@@ -43,7 +49,10 @@
import com.android.testutils.TestableNetworkCallback
import com.android.testutils.TestableNetworkOfferCallback
import com.android.testutils.runAsShell
+import kotlin.test.assertContains
import kotlin.test.assertEquals
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
import org.junit.After
import org.junit.Before
import org.junit.Test
@@ -83,6 +92,8 @@
private val handler = Handler(handlerThread.looper)
private val provider = NetworkProvider(context, handlerThread.looper, TAG)
+ private val registeredCallbacks = ArrayList<TestableNetworkCallback>()
+
@Before
fun setUp() {
runAsShell(NETWORK_SETTINGS) {
@@ -92,6 +103,7 @@
@After
fun tearDown() {
+ registeredCallbacks.forEach { cm.unregisterNetworkCallback(it) }
runAsShell(NETWORK_SETTINGS) {
// unregisterNetworkProvider unregisters all associated NetworkOffers.
cm.unregisterNetworkProvider(provider)
@@ -104,6 +116,13 @@
it.reservationId = resId
}
+ fun reserveNetwork(nr: NetworkRequest): TestableNetworkCallback {
+ return TestableNetworkCallback().also {
+ cm.reserveNetwork(nr, handler, it)
+ registeredCallbacks.add(it)
+ }
+ }
+
@Test
fun testReserveNetwork() {
// register blanket offer
@@ -112,8 +131,7 @@
provider.registerNetworkOffer(NETWORK_SCORE, BLANKET_CAPS, handler::post, blanketOffer)
}
- val cb = TestableNetworkCallback()
- cm.reserveNetwork(ETHERNET_REQUEST, handler, cb)
+ val cb = reserveNetwork(ETHERNET_REQUEST)
// validate the reservation matches the blanket offer.
val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
@@ -137,4 +155,28 @@
provider.unregisterNetworkOffer(reservedOffer)
cb.expect<Unavailable>()
}
+
+ @Test
+ fun testReserveL2capNetwork() {
+ val l2capReservationSpecifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+ .build()
+ val l2capRequest = NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_BLUETOOTH)
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
+ .setNetworkSpecifier(l2capReservationSpecifier)
+ .build()
+ val cb = runAsShell(CONNECTIVITY_USE_RESTRICTED_NETWORKS) {
+ reserveNetwork(l2capRequest)
+ }
+
+ val caps = cb.expect<Reserved>().caps
+ val reservedSpec = caps.networkSpecifier
+ assertTrue(reservedSpec is L2capNetworkSpecifier)
+ assertContains(0x80..0xFF, reservedSpec.psm, "PSM is outside of dynamic range")
+ assertEquals(HEADER_COMPRESSION_6LOWPAN, reservedSpec.headerCompression)
+ assertNull(reservedSpec.remoteAddress)
+ }
}
diff --git a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
index 375d604..437eb81 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
@@ -55,12 +55,14 @@
import com.android.networkstack.apishim.TelephonyManagerShimImpl
import com.android.server.BpfNetMaps
import com.android.server.ConnectivityService
+import com.android.server.L2capNetworkProvider
import com.android.server.NetworkAgentWrapper
import com.android.server.TestNetIdManager
import com.android.server.connectivity.CarrierPrivilegeAuthenticator
import com.android.server.connectivity.ConnectivityResources
import com.android.server.connectivity.MockableSystemProperties
import com.android.server.connectivity.MultinetworkPolicyTracker
+import com.android.server.connectivity.PermissionMonitor
import com.android.server.connectivity.ProxyTracker
import com.android.server.connectivity.SatelliteAccessController
import com.android.testutils.DevSdkIgnoreRunner
@@ -221,7 +223,7 @@
}
private inner class TestConnectivityService(deps: Dependencies) : ConnectivityService(
- context, dnsResolver, log, netd, deps)
+ context, dnsResolver, log, netd, deps, PermissionMonitorDependencies())
private inner class TestDependencies : ConnectivityService.Dependencies() {
override fun getNetworkStack() = networkStackClient
@@ -272,6 +274,12 @@
connectivityServiceInternalHandler: Handler
): SatelliteAccessController? = mock(
SatelliteAccessController::class.java)
+
+ override fun makeL2capNetworkProvider(context: Context) = null
+ }
+
+ private inner class PermissionMonitorDependencies : PermissionMonitor.Dependencies() {
+ override fun shouldEnforceLocalNetRestrictions(uid: Int) = false
}
@After
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index f7d7c87..b7aa387 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -154,6 +154,7 @@
import static android.net.Proxy.PROXY_CHANGE_ACTION;
import static android.net.RouteInfo.RTN_UNREACHABLE;
import static android.net.connectivity.ConnectivityCompatChanges.NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION;
+import static android.net.connectivity.ConnectivityCompatChanges.RESTRICT_LOCAL_NETWORK;
import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.PREFIX_OPERATION_ADDED;
import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.PREFIX_OPERATION_REMOVED;
import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_FAILURE;
@@ -257,6 +258,7 @@
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.admin.DevicePolicyManager;
+import android.app.compat.CompatChanges;
import android.app.usage.NetworkStatsManager;
import android.compat.testing.PlatformCompatChangeRule;
import android.content.BroadcastReceiver;
@@ -412,6 +414,7 @@
import com.android.server.ConnectivityService.NetworkRequestInfo;
import com.android.server.ConnectivityServiceTest.ConnectivityServiceDependencies.DestroySocketsWrapper;
import com.android.server.ConnectivityServiceTest.ConnectivityServiceDependencies.ReportedInterfaces;
+import com.android.server.L2capNetworkProvider;
import com.android.server.connectivity.ApplicationSelfCertifiedNetworkCapabilities;
import com.android.server.connectivity.AutomaticOnOffKeepaliveTracker;
import com.android.server.connectivity.CarrierPrivilegeAuthenticator;
@@ -425,6 +428,7 @@
import com.android.server.connectivity.NetworkAgentInfo;
import com.android.server.connectivity.NetworkNotificationManager;
import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
+import com.android.server.connectivity.PermissionMonitor;
import com.android.server.connectivity.ProxyTracker;
import com.android.server.connectivity.QosCallbackTracker;
import com.android.server.connectivity.SatelliteAccessController;
@@ -593,6 +597,7 @@
private MockContext mServiceContext;
private HandlerThread mCsHandlerThread;
private ConnectivityServiceDependencies mDeps;
+ private PermissionMonitorDependencies mPermDeps;
private AutomaticOnOffKeepaliveTrackerDependencies mAutoOnOffKeepaliveDependencies;
private ConnectivityService mService;
private WrappedConnectivityManager mCm;
@@ -1920,6 +1925,7 @@
doReturn(mResources).when(mockResContext).getResources();
ConnectivityResources.setResourcesContextForTest(mockResContext);
mDeps = new ConnectivityServiceDependencies(mockResContext);
+ mPermDeps = new PermissionMonitorDependencies();
doReturn(true).when(mMockKeepaliveTrackerDependencies)
.isAddressTranslationEnabled(mServiceContext);
doReturn(new ConnectivityResources(mockResContext)).when(mMockKeepaliveTrackerDependencies)
@@ -1932,7 +1938,7 @@
mMockDnsResolver,
mock(IpConnectivityLog.class),
mMockNetd,
- mDeps);
+ mDeps, mPermDeps);
mService.mLingerDelayMs = TEST_LINGER_DELAY_MS;
mService.mNascentDelayMs = TEST_NASCENT_DELAY_MS;
@@ -2381,6 +2387,18 @@
// Needed to mock out the dependency on DeviceConfig
return 15;
}
+
+ @Override
+ public L2capNetworkProvider makeL2capNetworkProvider(Context context) {
+ return null;
+ }
+ }
+
+ static class PermissionMonitorDependencies extends PermissionMonitor.Dependencies {
+ @Override
+ public boolean shouldEnforceLocalNetRestrictions(int uid) {
+ return false;
+ }
}
private class AutomaticOnOffKeepaliveTrackerDependencies
diff --git a/tests/unit/java/com/android/server/connectivity/NetworkPermissionsTest.kt b/tests/unit/java/com/android/server/connectivity/NetworkPermissionsTest.kt
new file mode 100644
index 0000000..8a9d288
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/NetworkPermissionsTest.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2025 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 android.net.INetd
+import android.os.Build
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class NetworkPermissionsTest {
+ @Test
+ fun test_networkTrafficPerms_correctValues() {
+ assertEquals(NetworkPermissions.PERMISSION_NONE, INetd.PERMISSION_NONE) /* 0 */
+ assertEquals(NetworkPermissions.PERMISSION_NETWORK, INetd.PERMISSION_NETWORK) /* 1 */
+ assertEquals(NetworkPermissions.PERMISSION_SYSTEM, INetd.PERMISSION_SYSTEM) /* 2 */
+ assertEquals(NetworkPermissions.TRAFFIC_PERMISSION_INTERNET, 4)
+ assertEquals(NetworkPermissions.TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS, 8)
+ assertEquals(NetworkPermissions.TRAFFIC_PERMISSION_UNINSTALLED, -1)
+ assertEquals(NetworkPermissions.TRAFFIC_PERMISSION_SDKSANDBOX_LOCALHOST, 16)
+ }
+
+ @Test
+ fun test_noOverridesInFlags() {
+ val permsList = listOf(
+ NetworkPermissions.PERMISSION_NONE,
+ NetworkPermissions.PERMISSION_NETWORK,
+ NetworkPermissions.PERMISSION_SYSTEM,
+ NetworkPermissions.TRAFFIC_PERMISSION_INTERNET,
+ NetworkPermissions.TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS,
+ NetworkPermissions.TRAFFIC_PERMISSION_SDKSANDBOX_LOCALHOST,
+ NetworkPermissions.TRAFFIC_PERMISSION_UNINSTALLED
+ )
+ assertFalse(hasDuplicates(permsList))
+ }
+
+ fun hasDuplicates(list: List<Int>): Boolean {
+ return list.distinct().size != list.size
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
index 55c68b7..ec9c6b0 100644
--- a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
@@ -21,6 +21,7 @@
import static android.Manifest.permission.CONNECTIVITY_INTERNAL;
import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
import static android.Manifest.permission.INTERNET;
+import static android.Manifest.permission.NEARBY_WIFI_DEVICES;
import static android.Manifest.permission.NETWORK_STACK;
import static android.Manifest.permission.OBSERVE_GRANT_REVOKE_PERMISSIONS;
import static android.Manifest.permission.UPDATE_DEVICE_STATS;
@@ -31,6 +32,7 @@
import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_REQUIRED;
import static android.content.pm.PackageManager.GET_PERMISSIONS;
import static android.content.pm.PackageManager.MATCH_ANY_USER;
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
import static android.net.ConnectivitySettingsManager.UIDS_ALLOWED_ON_RESTRICTED_NETWORKS;
import static android.net.INetd.PERMISSION_INTERNET;
import static android.net.INetd.PERMISSION_NETWORK;
@@ -39,16 +41,20 @@
import static android.net.INetd.PERMISSION_UNINSTALLED;
import static android.net.INetd.PERMISSION_UPDATE_DEVICE_STATS;
import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
+import static android.net.connectivity.ConnectivityCompatChanges.RESTRICT_LOCAL_NETWORK;
import static android.os.Process.SYSTEM_UID;
+import static android.permission.PermissionManager.PERMISSION_GRANTED;
import static com.android.server.connectivity.PermissionMonitor.isHigherNetworkPermission;
import static com.android.testutils.TestPermissionUtil.runAsShell;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import static junit.framework.Assert.fail;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
import static org.mockito.AdditionalMatchers.aryEq;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -68,6 +74,8 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.compat.testing.PlatformCompatChangeRule;
+import android.content.AttributionSource;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -85,7 +93,9 @@
import android.os.SystemConfigManager;
import android.os.UserHandle;
import android.os.UserManager;
+import android.permission.PermissionManager;
import android.provider.Settings;
+import android.util.ArraySet;
import android.util.SparseIntArray;
import androidx.annotation.NonNull;
@@ -102,9 +112,13 @@
import com.android.testutils.DevSdkIgnoreRunner;
import com.android.testutils.HandlerUtils;
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
import org.mockito.AdditionalAnswers;
import org.mockito.ArgumentCaptor;
@@ -121,6 +135,8 @@
@SmallTest
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
public class PermissionMonitorTest {
+ @Rule
+ public TestRule compatChangeRule = new PlatformCompatChangeRule();
private static final int MOCK_USER_ID1 = 0;
private static final int MOCK_USER_ID2 = 1;
private static final int MOCK_USER_ID3 = 2;
@@ -162,9 +178,14 @@
private static final int PERMISSION_TRAFFIC_ALL =
PERMISSION_INTERNET | PERMISSION_UPDATE_DEVICE_STATS;
private static final int TIMEOUT_MS = 2_000;
+ // The ACCESS_LOCAL_NETWORK permission is not available yet. For the time being, use
+ // NEARBY_WIFI_DEVICES as a means to develop, for expediency.
+ // TODO(b/375236298): remove this constant when the ACCESS_LOCAL_NETWORK permission is defined.
+ private static final String ACCESS_LOCAL_NETWORK = NEARBY_WIFI_DEVICES;
@Mock private Context mContext;
@Mock private PackageManager mPackageManager;
+ @Mock private PermissionManager mPermissionManager;
@Mock private INetd mNetdService;
@Mock private UserManager mUserManager;
@Mock private PermissionMonitor.Dependencies mDeps;
@@ -183,6 +204,7 @@
when(mContext.getPackageManager()).thenReturn(mPackageManager);
when(mContext.getSystemService(eq(Context.USER_SERVICE))).thenReturn(mUserManager);
doReturn(List.of(MOCK_USER1)).when(mUserManager).getUserHandles(eq(true));
+ when(mContext.getSystemService(PermissionManager.class)).thenReturn(mPermissionManager);
when(mContext.getSystemServiceName(SystemConfigManager.class))
.thenReturn(Context.SYSTEM_CONFIG_SERVICE);
when(mContext.getSystemService(Context.SYSTEM_CONFIG_SERVICE))
@@ -295,19 +317,28 @@
return result;
}
- private void buildAndMockPackageInfoWithPermissions(String packageName, int uid,
+ private PackageInfo buildAndMockPackageInfoWithPermissions(String packageName, int uid,
String... permissions) throws Exception {
final PackageInfo packageInfo = buildPackageInfo(packageName, uid, permissions);
// This will return the wrong UID for the package when queried with other users.
doReturn(packageInfo).when(mPackageManager)
.getPackageInfo(eq(packageName), anyInt() /* flag */);
+ if (BpfNetMaps.isAtLeast25Q2()) {
+ // Runtime permission checks for local net restrictions were introduced in 25Q2
+ for (String permission : permissions) {
+ doReturn(PERMISSION_GRANTED).when(mPermissionManager).checkPermissionForPreflight(
+ eq(permission),
+ argThat(attributionSource -> attributionSource.getUid() == uid));
+ }
+ }
final String[] oldPackages = mPackageManager.getPackagesForUid(uid);
// If it's duplicated package, no need to set it again.
- if (CollectionUtils.contains(oldPackages, packageName)) return;
+ if (CollectionUtils.contains(oldPackages, packageName)) return packageInfo;
// Combine the package if this uid is shared with other packages.
final String[] newPackages = appendElement(String.class, oldPackages, packageName);
doReturn(newPackages).when(mPackageManager).getPackagesForUid(eq(uid));
+ return packageInfo;
}
private void startMonitoring() {
@@ -342,7 +373,7 @@
private void addPackage(String packageName, int uid, String... permissions) throws Exception {
buildAndMockPackageInfoWithPermissions(packageName, uid, permissions);
- processOnHandlerThread(() -> mPermissionMonitor.onPackageAdded(packageName, uid));
+ onPackageAdded(packageName, uid);
}
private void removePackage(String packageName, int uid) {
@@ -354,7 +385,12 @@
final String[] newPackages = Arrays.stream(oldPackages).filter(e -> !e.equals(packageName))
.toArray(String[]::new);
doReturn(newPackages).when(mPackageManager).getPackagesForUid(eq(uid));
- processOnHandlerThread(() -> mPermissionMonitor.onPackageRemoved(packageName, uid));
+ if (BpfNetMaps.isAtLeast25Q2()){
+ // Runtime permission checks for local net restrictions were introduced in 25Q2
+ doReturn(PERMISSION_DENIED).when(mPermissionManager).checkPermissionForPreflight(
+ anyString(), argThat(as -> as.getUid() == uid));
+ }
+ onPackageRemoved(packageName, uid);
}
@Test
@@ -585,6 +621,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testHasUseBackgroundNetworksPermission() throws Exception {
assertFalse(mPermissionMonitor.hasUseBackgroundNetworksPermission(SYSTEM_UID));
assertBackgroundPermission(false, SYSTEM_PACKAGE1, SYSTEM_UID);
@@ -606,6 +643,7 @@
private class BpfMapMonitor {
private final SparseIntArray mAppIdsTrafficPermission = new SparseIntArray();
+ private final ArraySet<Integer> mLocalNetBlockedUids = new ArraySet<>();
private static final int DOES_NOT_EXIST = -2;
BpfMapMonitor(BpfNetMaps mockBpfmap) throws Exception {
@@ -618,6 +656,18 @@
}
return null;
}).when(mockBpfmap).setNetPermForUids(anyInt(), any(int[].class));
+ doAnswer((InvocationOnMock invocation) -> {
+ final Object[] args = invocation.getArguments();
+ final int uid = (int) args[0];
+ mLocalNetBlockedUids.add(uid);
+ return null;
+ }).when(mockBpfmap).addUidToLocalNetBlockMap(anyInt());
+ doAnswer((InvocationOnMock invocation) -> {
+ final Object[] args = invocation.getArguments();
+ final int uid = (int) args[0];
+ mLocalNetBlockedUids.remove(uid);
+ return null;
+ }).when(mockBpfmap).removeUidFromLocalNetBlockMap(anyInt());
}
public void expectTrafficPerm(int permission, Integer... appIds) {
@@ -642,6 +692,18 @@
}
}
}
+
+ public boolean hasLocalNetPermissions(int uid) {
+ return !mLocalNetBlockedUids.contains(uid);
+ }
+
+ public boolean isUidPresentInLocalNetBlockMap(int uid) {
+ return mLocalNetBlockedUids.contains(uid);
+ }
+
+ public boolean hasBlockedLocalNetForSandboxUid(int sandboxUid) {
+ return mLocalNetBlockedUids.contains(sandboxUid);
+ }
}
private class NetdMonitor {
@@ -725,6 +787,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testUserAndPackageAddRemove() throws Exception {
// MOCK_UID11: MOCK_PACKAGE1 only has network permission.
// SYSTEM_APP_UID11: SYSTEM_PACKAGE1 has system permission.
@@ -814,6 +877,48 @@
MOCK_APPID1);
}
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
+ public void testLocalNetRestrictions_onUserAdded() throws Exception {
+ assumeTrue(BpfNetMaps.isAtLeast25Q2());
+ doReturn(true).when(mDeps).shouldEnforceLocalNetRestrictions(anyInt());
+ when(mPermissionManager.checkPermissionForPreflight(
+ anyString(), any(AttributionSource.class))).thenReturn(PERMISSION_DENIED);
+ final PackageInfo packageInfo = buildAndMockPackageInfoWithPermissions(
+ MOCK_PACKAGE1, MOCK_UID11, CHANGE_NETWORK_STATE);
+ // Set package for all users on devices
+ doReturn(List.of(packageInfo)).when(mPackageManager)
+ .getInstalledPackagesAsUser(anyInt(), eq(MOCK_USER1.getIdentifier()));
+ onUserAdded(MOCK_USER1);
+
+ assertFalse(mBpfMapMonitor.hasLocalNetPermissions(MOCK_UID11));
+ if (hasSdkSandbox(MOCK_UID11)) {
+ assertTrue(mBpfMapMonitor.hasBlockedLocalNetForSandboxUid(
+ mProcessShim.toSdkSandboxUid(MOCK_UID11)));
+ }
+ }
+
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
+ public void testLocalNetRestrictions_onUserRemoved() throws Exception {
+ assumeTrue(BpfNetMaps.isAtLeast25Q2());
+ doReturn(true).when(mDeps).shouldEnforceLocalNetRestrictions(anyInt());
+ when(mPermissionManager.checkPermissionForPreflight(
+ anyString(), any(AttributionSource.class))).thenReturn(PERMISSION_DENIED);
+ final PackageInfo packageInfo = buildAndMockPackageInfoWithPermissions(
+ MOCK_PACKAGE1, MOCK_UID11, CHANGE_NETWORK_STATE);
+ // Set package for all users on devices
+ doReturn(List.of(packageInfo)).when(mPackageManager)
+ .getInstalledPackagesAsUser(anyInt(), eq(MOCK_USER1.getIdentifier()));
+ onUserAdded(MOCK_USER1);
+ assertFalse(mBpfMapMonitor.hasLocalNetPermissions(MOCK_UID11));
+
+ onUserRemoved(MOCK_USER1);
+ assertFalse(mBpfMapMonitor.isUidPresentInLocalNetBlockMap(MOCK_UID11));
+ }
+
private void doTestUidFilteringDuringVpnConnectDisconnectAndUidUpdates(@Nullable String ifName)
throws Exception {
doReturn(List.of(
@@ -860,11 +965,13 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdates() throws Exception {
doTestUidFilteringDuringVpnConnectDisconnectAndUidUpdates("tun0");
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdatesWithWildcard()
throws Exception {
doTestUidFilteringDuringVpnConnectDisconnectAndUidUpdates(null /* ifName */);
@@ -897,16 +1004,19 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testUidFilteringDuringPackageInstallAndUninstall() throws Exception {
doTestUidFilteringDuringPackageInstallAndUninstall("tun0");
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testUidFilteringDuringPackageInstallAndUninstallWithWildcard() throws Exception {
doTestUidFilteringDuringPackageInstallAndUninstall(null /* ifName */);
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testLockdownUidFilteringWithLockdownEnableDisable() {
doReturn(List.of(
buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
@@ -938,6 +1048,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testLockdownUidFilteringWithLockdownEnableDisableWithMultiAdd() {
doReturn(List.of(
buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
@@ -979,6 +1090,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testLockdownUidFilteringWithLockdownEnableDisableWithMultiAddAndOverlap() {
doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
CONNECTIVITY_USE_RESTRICTED_NETWORKS),
@@ -1039,6 +1151,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testLockdownUidFilteringWithLockdownEnableDisableWithDuplicates() {
doReturn(List.of(
buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
@@ -1073,6 +1186,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testLockdownUidFilteringWithInstallAndUnInstall() {
doReturn(List.of(
buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
@@ -1109,15 +1223,13 @@
// called multiple times with the uid corresponding to each user.
private void addPackageForUsers(UserHandle[] users, String packageName, int appId) {
for (final UserHandle user : users) {
- processOnHandlerThread(() ->
- mPermissionMonitor.onPackageAdded(packageName, user.getUid(appId)));
+ onPackageAdded(packageName, user.getUid(appId));
}
}
private void removePackageForUsers(UserHandle[] users, String packageName, int appId) {
for (final UserHandle user : users) {
- processOnHandlerThread(() ->
- mPermissionMonitor.onPackageRemoved(packageName, user.getUid(appId)));
+ onPackageRemoved(packageName, user.getUid(appId));
}
}
@@ -1165,6 +1277,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testPackageInstall() throws Exception {
addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET, UPDATE_DEVICE_STATS);
mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1);
@@ -1173,7 +1286,25 @@
mBpfMapMonitor.expectTrafficPerm(PERMISSION_INTERNET, MOCK_APPID2);
}
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
+ public void testLocalNetRestrictions_onPackageInstall() throws Exception {
+ assumeTrue(BpfNetMaps.isAtLeast25Q2());
+ doReturn(true).when(mDeps).shouldEnforceLocalNetRestrictions(anyInt());
+ when(mPermissionManager.checkPermissionForPreflight(
+ anyString(), any(AttributionSource.class))).thenReturn(PERMISSION_DENIED);
+ addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET);
+ assertFalse(mBpfMapMonitor.hasLocalNetPermissions(MOCK_UID11));
+
+ addPackage(MOCK_PACKAGE2, MOCK_UID12, ACCESS_LOCAL_NETWORK);
+ assertTrue(mBpfMapMonitor.hasLocalNetPermissions(MOCK_UID12));
+ if (hasSdkSandbox(MOCK_UID12)) assertTrue(mBpfMapMonitor.hasBlockedLocalNetForSandboxUid(
+ mProcessShim.toSdkSandboxUid(MOCK_UID12)));
+ }
+
+ @Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testPackageInstallSharedUid() throws Exception {
addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET, UPDATE_DEVICE_STATS);
mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1);
@@ -1185,6 +1316,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testPackageUninstallBasic() throws Exception {
addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET, UPDATE_DEVICE_STATS);
mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1);
@@ -1194,7 +1326,24 @@
mBpfMapMonitor.expectTrafficPerm(PERMISSION_UNINSTALLED, MOCK_APPID1);
}
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
+ public void testLocalNetRestrictions_onPackageUninstall() throws Exception {
+ assumeTrue(BpfNetMaps.isAtLeast25Q2());
+ doReturn(true).when(mDeps).shouldEnforceLocalNetRestrictions(anyInt());
+ when(mPermissionManager.checkPermissionForPreflight(
+ anyString(), any(AttributionSource.class))).thenReturn(PERMISSION_DENIED);
+ addPackage(MOCK_PACKAGE1, MOCK_UID11, ACCESS_LOCAL_NETWORK);
+ assertTrue(mBpfMapMonitor.hasLocalNetPermissions(MOCK_UID11));
+
+ when(mPackageManager.getPackagesForUid(MOCK_UID11)).thenReturn(new String[]{});
+ onPackageRemoved(MOCK_PACKAGE1, MOCK_UID11);
+ assertFalse(mBpfMapMonitor.isUidPresentInLocalNetBlockMap(MOCK_UID11));
+ }
+
+ @Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testPackageRemoveThenAdd() throws Exception {
addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET, UPDATE_DEVICE_STATS);
mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1);
@@ -1207,7 +1356,30 @@
mBpfMapMonitor.expectTrafficPerm(PERMISSION_INTERNET, MOCK_APPID1);
}
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
+ public void testLocalNetRestrictions_onPackageRemoveThenAdd() throws Exception {
+ assumeTrue(BpfNetMaps.isAtLeast25Q2());
+ doReturn(true).when(mDeps).shouldEnforceLocalNetRestrictions(anyInt());
+ when(mPermissionManager.checkPermissionForPreflight(
+ anyString(), any(AttributionSource.class))).thenReturn(PERMISSION_DENIED);
+ addPackage(MOCK_PACKAGE1, MOCK_UID11, ACCESS_LOCAL_NETWORK);
+ assertTrue(mBpfMapMonitor.hasLocalNetPermissions(MOCK_UID11));
+ if (hasSdkSandbox(MOCK_UID12)) assertTrue(mBpfMapMonitor.hasBlockedLocalNetForSandboxUid(
+ mProcessShim.toSdkSandboxUid(MOCK_UID11)));
+
+ removePackage(MOCK_PACKAGE1, MOCK_UID11);
+ assertFalse(mBpfMapMonitor.isUidPresentInLocalNetBlockMap(MOCK_UID11));
+
+ addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET);
+ assertFalse(mBpfMapMonitor.hasLocalNetPermissions(MOCK_UID11));
+ if (hasSdkSandbox(MOCK_UID12)) assertTrue(mBpfMapMonitor.hasBlockedLocalNetForSandboxUid(
+ mProcessShim.toSdkSandboxUid(MOCK_UID11)));
+ }
+
+ @Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testPackageUpdate() throws Exception {
addPackage(MOCK_PACKAGE1, MOCK_UID11);
mBpfMapMonitor.expectTrafficPerm(PERMISSION_NONE, MOCK_APPID1);
@@ -1217,6 +1389,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testPackageUninstallWithMultiplePackages() throws Exception {
addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET, UPDATE_DEVICE_STATS);
mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1);
@@ -1248,6 +1421,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testUpdateUidPermissionsFromSystemConfig() throws Exception {
when(mSystemConfigManager.getSystemPermissionUids(eq(INTERNET)))
.thenReturn(new int[]{ MOCK_UID11, MOCK_UID12 });
@@ -1287,6 +1461,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testIntentReceiver() throws Exception {
startMonitoring();
final BroadcastReceiver receiver = expectBroadcastReceiver(
@@ -1325,6 +1500,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testUidsAllowedOnRestrictedNetworksChanged() throws Exception {
startMonitoring();
final ContentObserver contentObserver = expectRegisterContentObserver(
@@ -1357,6 +1533,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testUidsAllowedOnRestrictedNetworksChangedWithSharedUid() throws Exception {
startMonitoring();
final ContentObserver contentObserver = expectRegisterContentObserver(
@@ -1390,6 +1567,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testUidsAllowedOnRestrictedNetworksChangedWithMultipleUsers() throws Exception {
startMonitoring();
final ContentObserver contentObserver = expectRegisterContentObserver(
@@ -1444,6 +1622,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testOnExternalApplicationsAvailable() throws Exception {
// Initial the permission state. MOCK_PACKAGE1 and MOCK_PACKAGE2 are installed on external
// and have different uids. There has no permission for both uids.
@@ -1475,6 +1654,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testOnExternalApplicationsAvailable_AppsNotRegisteredOnStartMonitoring()
throws Exception {
startMonitoring();
@@ -1502,6 +1682,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testOnExternalApplicationsAvailableWithSharedUid()
throws Exception {
// Initial the permission state. MOCK_PACKAGE1 and MOCK_PACKAGE2 are installed on external
@@ -1528,6 +1709,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testOnExternalApplicationsAvailableWithSharedUid_DifferentStorage()
throws Exception {
// Initial the permission state. MOCK_PACKAGE1 is installed on external storage and
@@ -1570,6 +1752,38 @@
assertFalse(isHigherNetworkPermission(PERMISSION_SYSTEM, PERMISSION_SYSTEM));
}
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
+ public void testLocalNetRestrictions_setPermChanges() throws Exception {
+ assumeTrue(BpfNetMaps.isAtLeast25Q2());
+ doReturn(true).when(mDeps).shouldEnforceLocalNetRestrictions(anyInt());
+ when(mPermissionManager.checkPermissionForPreflight(
+ anyString(), any(AttributionSource.class))).thenReturn(PERMISSION_DENIED);
+ addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET);
+ assertFalse(mBpfMapMonitor.hasLocalNetPermissions(MOCK_UID11));
+
+ // Mock permission grant
+ when(mPermissionManager.checkPermissionForPreflight(
+ eq(ACCESS_LOCAL_NETWORK),
+ argThat(attributionSource -> attributionSource.getUid() == MOCK_UID11)))
+ .thenReturn(PERMISSION_GRANTED);
+ mPermissionMonitor.setLocalNetworkPermissions(MOCK_UID11, null);
+ assertTrue(mBpfMapMonitor.hasLocalNetPermissions(MOCK_UID11));
+ if (hasSdkSandbox(MOCK_UID12)) assertTrue(mBpfMapMonitor.hasBlockedLocalNetForSandboxUid(
+ mProcessShim.toSdkSandboxUid(MOCK_UID11)));
+
+ // Mock permission denied
+ when(mPermissionManager.checkPermissionForPreflight(
+ eq(ACCESS_LOCAL_NETWORK),
+ argThat(attributionSource -> attributionSource.getUid() == MOCK_UID11)))
+ .thenReturn(PERMISSION_DENIED);
+ mPermissionMonitor.setLocalNetworkPermissions(MOCK_UID11, null);
+ assertFalse(mBpfMapMonitor.hasLocalNetPermissions(MOCK_UID11));
+ if (hasSdkSandbox(MOCK_UID12)) assertTrue(mBpfMapMonitor.hasBlockedLocalNetForSandboxUid(
+ mProcessShim.toSdkSandboxUid(MOCK_UID11)));
+ }
+
private void prepareMultiUserPackages() {
// MOCK_USER1 has installed 3 packages
// mockApp1 has no permission and share MOCK_APPID1.
@@ -1602,7 +1816,7 @@
private void addUserAndVerifyAppIdsPermissions(UserHandle user, int appId1Perm,
int appId2Perm, int appId3Perm) {
- processOnHandlerThread(() -> mPermissionMonitor.onUserAdded(user));
+ onUserAdded(user);
mBpfMapMonitor.expectTrafficPerm(appId1Perm, MOCK_APPID1);
mBpfMapMonitor.expectTrafficPerm(appId2Perm, MOCK_APPID2);
mBpfMapMonitor.expectTrafficPerm(appId3Perm, MOCK_APPID3);
@@ -1610,13 +1824,14 @@
private void removeUserAndVerifyAppIdsPermissions(UserHandle user, int appId1Perm,
int appId2Perm, int appId3Perm) {
- processOnHandlerThread(() -> mPermissionMonitor.onUserRemoved(user));
+ onUserRemoved(user);
mBpfMapMonitor.expectTrafficPerm(appId1Perm, MOCK_APPID1);
mBpfMapMonitor.expectTrafficPerm(appId2Perm, MOCK_APPID2);
mBpfMapMonitor.expectTrafficPerm(appId3Perm, MOCK_APPID3);
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testAppIdsTrafficPermission_UserAddedRemoved() {
prepareMultiUserPackages();
@@ -1650,6 +1865,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testAppIdsTrafficPermission_Multiuser_PackageAdded() throws Exception {
// Add two users with empty package list.
onUserAdded(MOCK_USER1);
@@ -1720,6 +1936,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testAppIdsTrafficPermission_Multiuser_PackageRemoved() throws Exception {
// Add two users with empty package list.
onUserAdded(MOCK_USER1);
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSL2capProviderTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSL2capProviderTest.kt
new file mode 100644
index 0000000..489c3ad
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSL2capProviderTest.kt
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2024 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
+
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothManager
+import android.bluetooth.BluetoothServerSocket
+import android.bluetooth.BluetoothSocket
+import android.net.L2capNetworkSpecifier
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_6LOWPAN
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_NONE
+import android.net.L2capNetworkSpecifier.ROLE_SERVER
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
+import android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH
+import android.net.NetworkRequest
+import android.net.NetworkSpecifier
+import android.os.Build
+import android.os.HandlerThread
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Reserved
+import com.android.testutils.RecorderCallback.CallbackEntry.Unavailable
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.waitForIdle
+import java.io.IOException
+import java.util.Optional
+import java.util.concurrent.LinkedBlockingQueue
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNull
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.doThrow
+import org.mockito.Mockito.mock
+
+private const val PSM = 0x85
+private val REMOTE_MAC = byteArrayOf(1, 2, 3, 4, 5, 6)
+private val REQUEST = NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_BLUETOOTH)
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
+ .build()
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRunner.MonitorThreadLeak
+class CSL2capProviderTest : CSTest() {
+ private val btAdapter = mock<BluetoothAdapter>()
+ private val btServerSocket = mock<BluetoothServerSocket>()
+ private val btSocket = mock<BluetoothSocket>()
+ private val providerDeps = mock<L2capNetworkProvider.Dependencies>()
+ // BlockingQueue does not support put(null) operations, as null is used as an internal sentinel
+ // value. Therefore, use Optional<BluetoothSocket> where an empty optional signals the
+ // BluetoothServerSocket#close() operation.
+ private val acceptQueue = LinkedBlockingQueue<Optional<BluetoothSocket>>()
+
+ private val handlerThread = HandlerThread("CSL2capProviderTest thread").apply { start() }
+ private val registeredCallbacks = ArrayList<TestableNetworkCallback>()
+
+ // Requires Dependencies mock to be setup before creation.
+ private lateinit var provider: L2capNetworkProvider
+
+ @Before
+ fun innerSetUp() {
+ doReturn(btAdapter).`when`(bluetoothManager).getAdapter()
+ doReturn(btServerSocket).`when`(btAdapter).listenUsingInsecureL2capChannel()
+ doReturn(PSM).`when`(btServerSocket).getPsm()
+
+ doAnswer {
+ val sock = acceptQueue.take()
+ if (sock == null || !sock.isPresent()) throw IOException()
+ sock.get()
+ }.`when`(btServerSocket).accept()
+
+ doAnswer {
+ acceptQueue.put(Optional.empty())
+ }.`when`(btServerSocket).close()
+
+ doReturn(handlerThread).`when`(providerDeps).getHandlerThread()
+ provider = L2capNetworkProvider(providerDeps, context)
+ provider.start()
+ }
+
+ @After
+ fun innerTearDown() {
+ // Unregistering a callback which has previously been unregistered by virtue of receiving
+ // onUnavailable is a noop.
+ registeredCallbacks.forEach { cm.unregisterNetworkCallback(it) }
+ // Wait for CS handler idle, meaning the unregisterNetworkCallback has been processed and
+ // L2capNetworkProvider has been notified.
+ waitForIdle()
+
+ // While quitSafely() effectively waits for idle, it is not enough, because the tear down
+ // path itself posts on the handler thread. This means that waitForIdle() needs to run
+ // twice. The first time, to ensure all active threads have been joined, and the second time
+ // to run all associated clean up actions.
+ handlerThread.waitForIdle(HANDLER_TIMEOUT_MS)
+ handlerThread.quitSafely()
+ handlerThread.join()
+ }
+
+ private fun reserveNetwork(nr: NetworkRequest) = TestableNetworkCallback().also {
+ cm.reserveNetwork(nr, csHandler, it)
+ registeredCallbacks.add(it)
+ }
+
+ private fun requestNetwork(nr: NetworkRequest) = TestableNetworkCallback().also {
+ cm.requestNetwork(nr, it, csHandler)
+ registeredCallbacks.add(it)
+ }
+
+ private fun NetworkRequest.copyWithSpecifier(specifier: NetworkSpecifier): NetworkRequest {
+ // Note: NetworkRequest.Builder(NetworkRequest) *does not* perform a defensive copy but
+ // changes the underlying request.
+ return NetworkRequest.Builder(NetworkRequest(this))
+ .setNetworkSpecifier(specifier)
+ .build()
+ }
+
+ @Test
+ fun testReservation() {
+ val l2capServerSpecifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+ .build()
+ val l2capReservation = REQUEST.copyWithSpecifier(l2capServerSpecifier)
+ val reservationCb = reserveNetwork(l2capReservation)
+
+ val reservedCaps = reservationCb.expect<Reserved>().caps
+ val reservedSpec = reservedCaps.networkSpecifier as L2capNetworkSpecifier
+
+ assertEquals(PSM, reservedSpec.getPsm())
+ assertEquals(HEADER_COMPRESSION_6LOWPAN, reservedSpec.headerCompression)
+ assertNull(reservedSpec.remoteAddress)
+
+ reservationCb.assertNoCallback()
+ }
+
+ @Test
+ fun testBlanketOffer_reservationWithoutSpecifier() {
+ reserveNetwork(REQUEST).assertNoCallback()
+ }
+
+ @Test
+ fun testBlanketOffer_reservationWithCorrectSpecifier() {
+ var specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+ .build()
+ var nr = REQUEST.copyWithSpecifier(specifier)
+ reserveNetwork(nr).expect<Reserved>()
+
+ specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(HEADER_COMPRESSION_NONE)
+ .build()
+ nr = REQUEST.copyWithSpecifier(specifier)
+ reserveNetwork(nr).expect<Reserved>()
+ }
+
+ @Test
+ fun testBlanketOffer_reservationWithIncorrectSpecifier() {
+ var specifier = L2capNetworkSpecifier.Builder().build()
+ var nr = REQUEST.copyWithSpecifier(specifier)
+ reserveNetwork(nr).assertNoCallback()
+
+ specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .build()
+ nr = REQUEST.copyWithSpecifier(specifier)
+ reserveNetwork(nr).assertNoCallback()
+
+ specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(HEADER_COMPRESSION_NONE)
+ .setPsm(0x81)
+ .build()
+ nr = REQUEST.copyWithSpecifier(specifier)
+ reserveNetwork(nr).assertNoCallback()
+
+ specifier = L2capNetworkSpecifier.Builder()
+ .setHeaderCompression(HEADER_COMPRESSION_NONE)
+ .build()
+ nr = REQUEST.copyWithSpecifier(specifier)
+ reserveNetwork(nr).assertNoCallback()
+ }
+
+ @Test
+ fun testBluetoothException_listenUsingInsecureL2capChannelThrows() {
+ doThrow(IOException()).`when`(btAdapter).listenUsingInsecureL2capChannel()
+ var specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+ .build()
+ var nr = REQUEST.copyWithSpecifier(specifier)
+ reserveNetwork(nr).expect<Unavailable>()
+
+ doReturn(btServerSocket).`when`(btAdapter).listenUsingInsecureL2capChannel()
+ reserveNetwork(nr).expect<Reserved>()
+ }
+
+ @Test
+ fun testBluetoothException_acceptThrows() {
+ doThrow(IOException()).`when`(btServerSocket).accept()
+ var specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+ .build()
+ var nr = REQUEST.copyWithSpecifier(specifier)
+ val cb = reserveNetwork(nr)
+ cb.expect<Reserved>()
+ cb.expect<Unavailable>()
+
+ // BluetoothServerSocket#close() puts Optional.empty() on the acceptQueue.
+ acceptQueue.clear()
+ doAnswer {
+ val sock = acceptQueue.take()
+ assertFalse(sock.isPresent())
+ throw IOException() // to indicate the socket was closed.
+ }.`when`(btServerSocket).accept()
+ val cb2 = reserveNetwork(nr)
+ cb2.expect<Reserved>()
+ cb2.assertNoCallback()
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
index 83fff87..3583f84 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
@@ -54,9 +54,7 @@
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.never
import org.mockito.Mockito.timeout
-import org.mockito.Mockito.times
import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyNoMoreInteractions
private const val TIMEOUT_MS = 200L
private const val MEDIUM_TIMEOUT_MS = 1_000L
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalNetworkProtectionTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalNetworkProtectionTest.kt
index 73e7515..5bf6e04 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSLocalNetworkProtectionTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalNetworkProtectionTest.kt
@@ -190,6 +190,106 @@
}
@Test
+ fun testStackedLinkPropertiesWithDifferentLinkAddresses_AddressAddedInBpfMap() {
+ val nr = nr(TRANSPORT_WIFI)
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(nr, cb)
+
+ val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
+ val wifiLp2 = lp(WIFI_IFNAME_2, LOCAL_IPV4_LINK_ADDRRESS_1)
+ // Adding stacked link
+ wifiLp.addStackedLink(wifiLp2)
+ val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+ wifiAgent.connect()
+ cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+ // Multicast and Broadcast address should always be populated in local_net_access map
+ verifyPopulationOfMulticastAndBroadcastAddress()
+ // Verifying IPv6 address should be populated in local_net_access map
+ verify(bpfNetMaps).addLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+ eq(WIFI_IFNAME),
+ eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+ eq(0),
+ eq(0),
+ eq(false)
+ )
+
+ // Multicast and Broadcast address should always be populated on stacked link
+ // in local_net_access map
+ verifyPopulationOfMulticastAndBroadcastAddress(WIFI_IFNAME_2)
+ // Verifying IPv4 matching prefix(10.0.0.0/8) should be populated as part of stacked link
+ // in local_net_access map
+ verify(bpfNetMaps).addLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV4 + 8),
+ eq(WIFI_IFNAME_2),
+ eq(InetAddresses.parseNumericAddress("10.0.0.0")),
+ eq(0),
+ eq(0),
+ eq(false)
+ )
+ // As both addresses are in stacked links, so no address should be removed from the map.
+ verify(bpfNetMaps, never()).removeLocalNetAccess(any(), any(), any(), any(), any())
+ }
+
+ @Test
+ fun testRemovingStackedLinkProperties_AddressRemovedInBpfMap() {
+ val nr = nr(TRANSPORT_WIFI)
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(nr, cb)
+
+ val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
+ val wifiLp2 = lp(WIFI_IFNAME_2, LOCAL_IPV4_LINK_ADDRRESS_1)
+ // populating stacked link
+ wifiLp.addStackedLink(wifiLp2)
+ val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+ wifiAgent.connect()
+ cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+ // Multicast and Broadcast address should always be populated in local_net_access map
+ verifyPopulationOfMulticastAndBroadcastAddress()
+ // Verifying IPv6 address should be populated in local_net_access map
+ verify(bpfNetMaps).addLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+ eq(WIFI_IFNAME),
+ eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+ eq(0),
+ eq(0),
+ eq(false)
+ )
+
+ // Multicast and Broadcast address should always be populated on stacked link
+ // in local_net_access map
+ verifyPopulationOfMulticastAndBroadcastAddress(WIFI_IFNAME_2)
+ // Verifying IPv4 matching prefix(10.0.0.0/8) should be populated as part of stacked link
+ // in local_net_access map
+ verify(bpfNetMaps).addLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV4 + 8),
+ eq(WIFI_IFNAME_2),
+ eq(InetAddresses.parseNumericAddress("10.0.0.0")),
+ eq(0),
+ eq(0),
+ eq(false)
+ )
+ // As both addresses are in stacked links, so no address should be removed from the map.
+ verify(bpfNetMaps, never()).removeLocalNetAccess(any(), any(), any(), any(), any())
+
+ // replacing link properties without stacked links
+ val wifiLp_3 = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
+ wifiAgent.sendLinkProperties(wifiLp_3)
+ cb.expect<LinkPropertiesChanged>(wifiAgent.network)
+
+ // As both stacked links is removed, 10.0.0.0/8 should be removed from local_net_access map.
+ verify(bpfNetMaps).removeLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV4 + 8),
+ eq(WIFI_IFNAME_2),
+ eq(InetAddresses.parseNumericAddress("10.0.0.0")),
+ eq(0),
+ eq(0)
+ )
+ }
+
+ @Test
fun testChangeLinkPropertiesWithLinkAddressesInSameRange_AddressIntactInBpfMap() {
val nr = nr(TRANSPORT_WIFI)
val cb = TestableNetworkCallback()
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
index ae196a6..557bfd6 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -18,6 +18,7 @@
import android.app.AlarmManager
import android.app.AppOpsManager
+import android.bluetooth.BluetoothManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
@@ -71,6 +72,7 @@
import com.android.server.connectivity.MultinetworkPolicyTracker
import com.android.server.connectivity.MultinetworkPolicyTrackerTestDependencies
import com.android.server.connectivity.NetworkRequestStateStatsMetrics
+import com.android.server.connectivity.PermissionMonitor
import com.android.server.connectivity.ProxyTracker
import com.android.server.connectivity.SatelliteAccessController
import com.android.testutils.visibleOnHandlerThread
@@ -209,12 +211,14 @@
doReturn(true).`when`(it).isDataCapable()
}
val subscriptionManager = mock<SubscriptionManager>()
+ val bluetoothManager = mock<BluetoothManager>()
val multicastRoutingCoordinatorService = mock<MulticastRoutingCoordinatorService>()
val satelliteAccessController = mock<SatelliteAccessController>()
val destroySocketsWrapper = mock<DestroySocketsWrapper>()
val deps = CSDeps()
+ val permDeps = PermDeps()
// Initializations that start threads are done from setUp to avoid thread leak
lateinit var alarmHandlerThread: HandlerThread
@@ -251,7 +255,9 @@
alarmHandlerThread = HandlerThread("TestAlarmManager").also { it.start() }
alarmManager = makeMockAlarmManager(alarmHandlerThread)
- service = makeConnectivityService(context, netd, deps).also { it.systemReadyInternal() }
+ service = makeConnectivityService(context, netd, deps, permDeps).also {
+ it.systemReadyInternal()
+ }
cm = ConnectivityManager(context, service)
// csHandler initialization must be after makeConnectivityService since ConnectivityService
// constructor starts csHandlerThread
@@ -393,6 +399,12 @@
// Call mocked destroyLiveTcpSocketsByOwnerUids so that test can verify this method call
destroySocketsWrapper.destroyLiveTcpSocketsByOwnerUids(ownerUids)
}
+
+ override fun makeL2capNetworkProvider(context: Context) = null
+ }
+
+ inner class PermDeps : PermissionMonitor.Dependencies() {
+ override fun shouldEnforceLocalNetRestrictions(uid: Int) = false
}
inner class CSContext(base: Context) : BroadcastInterceptingContext(base) {
@@ -503,6 +515,7 @@
Context.BATTERY_STATS_SERVICE -> batteryManager
Context.STATS_MANAGER -> null // Stats manager is final and can't be mocked
Context.APP_OPS_SERVICE -> appOpsManager
+ Context.BLUETOOTH_SERVICE -> bluetoothManager
else -> super.getSystemService(serviceName)
}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
index 8ff790c..a53d430 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
@@ -23,6 +23,7 @@
import android.content.Context
import android.content.pm.PackageManager
import android.content.pm.PackageManager.FEATURE_BLUETOOTH
+import android.content.pm.PackageManager.FEATURE_BLUETOOTH_LE
import android.content.pm.PackageManager.FEATURE_ETHERNET
import android.content.pm.PackageManager.FEATURE_WIFI
import android.content.pm.PackageManager.FEATURE_WIFI_DIRECT
@@ -53,6 +54,7 @@
import com.android.modules.utils.build.SdkLevel
import com.android.server.ConnectivityService.Dependencies
import com.android.server.connectivity.ConnectivityResources
+import com.android.server.connectivity.PermissionMonitor
import kotlin.test.fail
import org.mockito.ArgumentMatchers
import org.mockito.ArgumentMatchers.any
@@ -103,7 +105,13 @@
}
internal fun makeMockPackageManager(realContext: Context) = mock<PackageManager>().also { pm ->
- val supported = listOf(FEATURE_WIFI, FEATURE_WIFI_DIRECT, FEATURE_BLUETOOTH, FEATURE_ETHERNET)
+ val supported = listOf(
+ FEATURE_WIFI,
+ FEATURE_WIFI_DIRECT,
+ FEATURE_BLUETOOTH,
+ FEATURE_BLUETOOTH_LE,
+ FEATURE_ETHERNET
+ )
doReturn(true).`when`(pm).hasSystemFeature(argThat { supported.contains(it) })
val myPackageName = realContext.packageName
val myPackageInfo = realContext.packageManager.getPackageInfo(myPackageName,
@@ -185,13 +193,14 @@
private val TEST_LINGER_DELAY_MS = 400
private val TEST_NASCENT_DELAY_MS = 300
-internal fun makeConnectivityService(context: Context, netd: INetd, deps: Dependencies) =
+internal fun makeConnectivityService(context: Context, netd: INetd, deps: Dependencies,
+ mPermDeps: PermissionMonitor.Dependencies) =
ConnectivityService(
context,
mock<IDnsResolver>(),
mock<IpConnectivityLog>(),
netd,
- deps).also {
+ deps, mPermDeps).also {
it.mLingerDelayMs = TEST_LINGER_DELAY_MS
it.mNascentDelayMs = TEST_NASCENT_DELAY_MS
}
diff --git a/tests/unit/java/com/android/server/net/HeaderCompressionUtilsTest.kt b/tests/unit/java/com/android/server/net/HeaderCompressionUtilsTest.kt
new file mode 100644
index 0000000..8431194
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/HeaderCompressionUtilsTest.kt
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2025 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.net
+
+import android.os.Build
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.internal.util.HexDump
+import com.google.common.truth.Truth.assertThat
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val TIMEOUT = 1000L
+
+@ConnectivityModuleTest
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class HeaderCompressionUtilsTest {
+
+ private fun decompressHex(hex: String): ByteArray {
+ val bytes = HexDump.hexStringToByteArray(hex)
+ val buf = bytes.copyOf(1500)
+ val newLen = HeaderCompressionUtils.decompress6lowpan(buf, bytes.size)
+ return buf.copyOf(newLen)
+ }
+
+ private fun compressHex(hex: String): ByteArray {
+ val buf = HexDump.hexStringToByteArray(hex)
+ val newLen = HeaderCompressionUtils.compress6lowpan(buf, buf.size)
+ return buf.copyOf(newLen)
+ }
+
+ private fun String.decodeHex() = HexDump.hexStringToByteArray(this)
+
+ @Test
+ fun testHeaderDecompression() {
+ // TF: 00, NH: 0, HLIM: 00, CID: 0, SAC: 0, SAM: 00, M: 0, DAC: 0, DAM: 00
+ var input = "6000" +
+ "ccf" + // ECN + DSCP + 4-bit Pad (here "f")
+ "12345" + // flow label
+ "11" + // next header
+ "e7" + // hop limit
+ "abcdef1234567890abcdef1234567890" + // source
+ "aaabbbcccdddeeefff00011122233344" + // dest
+ "abcd" // payload
+
+ var output = "6" + // version
+ "cc" + // traffic class
+ "12345" + // flow label
+ "0002" + // payload length
+ "11" + // next header
+ "e7" + // hop limit
+ "abcdef1234567890abcdef1234567890" + // source
+ "aaabbbcccdddeeefff00011122233344" + // dest
+ "abcd" // payload
+ assertThat(decompressHex(input)).isEqualTo(output.decodeHex())
+
+ // TF: 01, NH: 0, HLIM: 01, CID: 0, SAC: 0, SAM: 01, M: 0, DAC: 0, DAM: 01
+ input = "6911" +
+ "5" + // ECN + 2-bit pad (here "1")
+ "f100e" + // flow label
+ "42" + // next header
+ "1102030405060708" + // source
+ "aa0b0c0d0e0f1011" + // dest
+ "abcd" // payload
+
+ output = "6" + // version
+ "01" + // traffic class
+ "f100e" + // flow label
+ "0002" + // payload length
+ "42" + // next header
+ "01" + // hop limit
+ "fe800000000000001102030405060708" + // source
+ "fe80000000000000aa0b0c0d0e0f1011" + // dest
+ "abcd" // payload
+ assertThat(decompressHex(input)).isEqualTo(output.decodeHex())
+
+ // TF: 10, NH: 0, HLIM: 10, CID: 0, SAC: 0, SAM: 10, M: 0, DAC: 0, DAM: 10
+ input = "7222" +
+ "cc" + // traffic class
+ "43" + // next header
+ "1234" + // source
+ "abcd" + // dest
+ "abcdef" // payload
+
+ output = "6" + // version
+ "cc" + // traffic class
+ "00000" + // flow label
+ "0003" + // payload length
+ "43" + // next header
+ "40" + // hop limit
+ "fe80000000000000000000fffe001234" + // source
+ "fe80000000000000000000fffe00abcd" + // dest
+ "abcdef" // payload
+ assertThat(decompressHex(input)).isEqualTo(output.decodeHex())
+
+ // TF: 11, NH: 0, HLIM: 11, CID: 0, SAC: 0, SAM: 10, M: 1, DAC: 0, DAM: 00
+ input = "7b28" +
+ "44" + // next header
+ "1234" + // source
+ "ff020000000000000000000000000001" + // dest
+ "abcdef01" // payload
+
+ output = "6" + // version
+ "00" + // traffic class
+ "00000" + // flow label
+ "0004" + // payload length
+ "44" + // next header
+ "ff" + // hop limit
+ "fe80000000000000000000fffe001234" + // source
+ "ff020000000000000000000000000001" + // dest
+ "abcdef01" // payload
+ assertThat(decompressHex(input)).isEqualTo(output.decodeHex())
+
+ // TF: 11, NH: 0, HLIM: 11, CID: 0, SAC: 0, SAM: 10, M: 1, DAC: 0, DAM: 01
+ input = "7b29" +
+ "44" + // next header
+ "1234" + // source
+ "02abcdef1234" + // dest
+ "abcdef01" // payload
+
+ output = "6" + // version
+ "00" + // traffic class
+ "00000" + // flow label
+ "0004" + // payload length
+ "44" + // next header
+ "ff" + // hop limit
+ "fe80000000000000000000fffe001234" + // source
+ "ff02000000000000000000abcdef1234" + // dest
+ "abcdef01" // payload
+ assertThat(decompressHex(input)).isEqualTo(output.decodeHex())
+
+ // TF: 11, NH: 0, HLIM: 11, CID: 0, SAC: 0, SAM: 10, M: 1, DAC: 0, DAM: 10
+ input = "7b2a" +
+ "44" + // next header
+ "1234" + // source
+ "ee123456" + // dest
+ "abcdef01" // payload
+
+ output = "6" + // version
+ "00" + // traffic class
+ "00000" + // flow label
+ "0004" + // payload length
+ "44" + // next header
+ "ff" + // hop limit
+ "fe80000000000000000000fffe001234" + // source
+ "ffee0000000000000000000000123456" + // dest
+ "abcdef01" // payload
+ assertThat(decompressHex(input)).isEqualTo(output.decodeHex())
+
+ // TF: 11, NH: 0, HLIM: 11, CID: 0, SAC: 0, SAM: 10, M: 1, DAC: 0, DAM: 11
+ input = "7b2b" +
+ "44" + // next header
+ "1234" + // source
+ "89" + // dest
+ "abcdef01" // payload
+
+ output = "6" + // version
+ "00" + // traffic class
+ "00000" + // flow label
+ "0004" + // payload length
+ "44" + // next header
+ "ff" + // hop limit
+ "fe80000000000000000000fffe001234" + // source
+ "ff020000000000000000000000000089" + // dest
+ "abcdef01" // payload
+ assertThat(decompressHex(input)).isEqualTo(output.decodeHex())
+ }
+
+ @Test
+ fun testHeaderCompression() {
+ val input = "60120304000011fffe800000000000000000000000000001fe800000000000000000000000000002"
+ val output = "60000102030411fffe800000000000000000000000000001fe800000000000000000000000000002"
+ assertThat(compressHex(input)).isEqualTo(output.decodeHex())
+ }
+}
diff --git a/tests/unit/java/com/android/server/net/L2capPacketForwarderTest.kt b/tests/unit/java/com/android/server/net/L2capPacketForwarderTest.kt
index b3095ee..e261732 100644
--- a/tests/unit/java/com/android/server/net/L2capPacketForwarderTest.kt
+++ b/tests/unit/java/com/android/server/net/L2capPacketForwarderTest.kt
@@ -184,6 +184,7 @@
handler,
FdWrapper(ParcelFileDescriptor(tunFds[0])),
BluetoothSocketWrapper(bluetoothSocket),
+ false /* compressHeaders */,
callback
)
}
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index b528480..697bf9e 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -163,12 +163,13 @@
import com.android.net.module.util.BpfDump;
import com.android.net.module.util.IBpfMap;
import com.android.net.module.util.LocationPermissionChecker;
+import com.android.net.module.util.SkDestroyListener;
import com.android.net.module.util.Struct;
import com.android.net.module.util.Struct.S32;
import com.android.net.module.util.Struct.U8;
import com.android.net.module.util.bpf.CookieTagMapKey;
import com.android.net.module.util.bpf.CookieTagMapValue;
-import com.android.server.BpfNetMaps;
+import com.android.net.module.util.netlink.InetDiagMessage;
import com.android.server.connectivity.ConnectivityResources;
import com.android.server.net.NetworkStatsService.AlertObserver;
import com.android.server.net.NetworkStatsService.NetworkStatsSettings;
@@ -211,6 +212,7 @@
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
/**
* Tests for {@link NetworkStatsService}.
@@ -286,8 +288,6 @@
private LocationPermissionChecker mLocationPermissionChecker;
private TestBpfMap<S32, U8> mUidCounterSetMap = spy(new TestBpfMap<>(S32.class, U8.class));
@Mock
- private BpfNetMaps mBpfNetMaps;
- @Mock
private SkDestroyListener mSkDestroyListener;
private TestBpfMap<CookieTagMapKey, CookieTagMapValue> mCookieTagMap = new TestBpfMap<>(
@@ -608,13 +608,8 @@
}
@Override
- public BpfNetMaps makeBpfNetMaps(Context ctx) {
- return mBpfNetMaps;
- }
-
- @Override
- public SkDestroyListener makeSkDestroyListener(
- IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap, Handler handler) {
+ public SkDestroyListener makeSkDestroyListener(Consumer<InetDiagMessage> consumer,
+ Handler handler) {
return mSkDestroyListener;
}
diff --git a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
index d41550b..7a5895f 100644
--- a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
+++ b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
@@ -130,6 +130,7 @@
public void setUp() throws Exception {
mExecutor = Executors.newSingleThreadExecutor();
mOtCtl = new OtDaemonController();
+ mController.setEnabledAndWait(true);
mController.leaveAndWait();
// TODO: b/323301831 - This is a workaround to avoid unnecessary delay to re-form a network