Merge "Fix wrong condition for blocked status tracking wtf log" into main
diff --git a/OWNERS_core_networking b/OWNERS_core_networking
index 078ccde..4b73639 100644
--- a/OWNERS_core_networking
+++ b/OWNERS_core_networking
@@ -1,4 +1,5 @@
jchalard@google.com
+jimictw@google.com
junyulai@google.com
lorenzo@google.com
maze@google.com
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 1d2041b..8592af2 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -77,7 +77,7 @@
"name": "libnetworkstats_test"
},
{
- "name": "CtsTetheringTestLatestSdk",
+ "name": "CtsTetheringTest",
"options": [
{
"exclude-annotation": "com.android.testutils.NetworkStackModuleTest"
@@ -226,6 +226,9 @@
},
{
"name": "FrameworksNetIntegrationTests"
+ },
+ {
+ "name": "CtsTetheringTest"
}
],
"postsubmit": [
@@ -391,7 +394,7 @@
"name": "libnetworkstats_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
},
{
- "name": "CtsTetheringTestLatestSdk[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
+ "name": "CtsTetheringTest[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
"options": [
{
"exclude-annotation": "com.android.testutils.NetworkStackModuleTest"
@@ -406,7 +409,7 @@
"keywords": ["sim"]
},
{
- "name": "CtsTetheringTestLatestSdk[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
+ "name": "CtsTetheringTest[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
"keywords": ["sim"],
"options": [
{
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 4d173a5..091849b 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -58,6 +58,7 @@
":framework-connectivity-shared-srcs",
":services-tethering-shared-srcs",
":statslog-connectivity-java-gen",
+ ":statslog-framework-connectivity-java-gen",
":statslog-tethering-java-gen",
],
static_libs: [
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 19dd492..2878f79 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -102,15 +102,14 @@
"dscpPolicy.o",
"netd.o",
"offload.o",
- "offload@mainline.o",
"test.o",
- "test@mainline.o",
],
apps: [
"ServiceConnectivityResources",
],
prebuilts: [
"current_sdkinfo",
+ "netbpfload.31rc",
"netbpfload.33rc",
"netbpfload.35rc",
"ot-daemon.34rc",
diff --git a/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl b/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl
index 77e78bd..7d244e2 100644
--- a/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl
+++ b/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl
@@ -18,6 +18,7 @@
import android.net.IIntResultListener;
import android.net.ITetheringEventCallback;
import android.net.TetheringRequestParcel;
+import android.net.TetheringManager.TetheringRequest;
import android.os.ResultReceiver;
/** @hide */
@@ -37,6 +38,9 @@
void stopTethering(int type, String callerPkg, String callingAttributionTag,
IIntResultListener receiver);
+ void stopTetheringRequest(in TetheringRequest request, String callerPkg,
+ String callingAttributionTag, IIntResultListener receiver);
+
void requestLatestTetheringEntitlementResult(int type, in ResultReceiver receiver,
boolean showEntitlementUi, String callerPkg, String callingAttributionTag);
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index 21f36e8..0ac97f0 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -333,6 +333,11 @@
public static final int TETHER_ERROR_UNKNOWN_REQUEST = 17;
@FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
public static final int TETHER_ERROR_DUPLICATE_REQUEST = 18;
+ /**
+ * Never used outside Tethering.java.
+ * @hide
+ */
+ public static final int TETHER_ERROR_BLUETOOTH_SERVICE_PENDING = 19;
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@@ -807,6 +812,46 @@
*/
@SuppressLint("UnflaggedApi")
public static final class TetheringRequest implements Parcelable {
+ /**
+ * Tethering started by an explicit call to startTethering.
+ * @hide
+ */
+ public static final int REQUEST_TYPE_EXPLICIT = 0;
+
+ /**
+ * Tethering implicitly started by broadcasts (LOHS and P2P). Can never be pending.
+ * @hide
+ */
+ public static final int REQUEST_TYPE_IMPLICIT = 1;
+
+ /**
+ * Tethering started by the legacy tether() call. Can only happen on V-.
+ * @hide
+ */
+ public static final int REQUEST_TYPE_LEGACY = 2;
+
+ /**
+ * Tethering started but there was no pending request found. This may happen if Tethering is
+ * started and immediately stopped before the link layer goes up, or if we get a link layer
+ * event without a prior call to startTethering (e.g. adb shell cmd wifi start-softap).
+ * @hide
+ */
+ public static final int REQUEST_TYPE_PLACEHOLDER = 3;
+
+ /**
+ * Type of request, used to keep track of whether the request was explicitly sent by
+ * startTethering, implicitly created by broadcasts, or via legacy tether().
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "TYPE_", value = {
+ REQUEST_TYPE_EXPLICIT,
+ REQUEST_TYPE_IMPLICIT,
+ REQUEST_TYPE_LEGACY,
+ REQUEST_TYPE_PLACEHOLDER,
+ })
+ public @interface RequestType {}
+
/** A configuration set for TetheringRequest. */
private final TetheringRequestParcel mRequestParcel;
@@ -866,6 +911,7 @@
mBuilderParcel.uid = Process.INVALID_UID;
mBuilderParcel.softApConfig = null;
mBuilderParcel.interfaceName = null;
+ mBuilderParcel.requestType = REQUEST_TYPE_EXPLICIT;
}
/**
@@ -1161,6 +1207,14 @@
}
/**
+ * Get the type of the request.
+ * @hide
+ */
+ public @RequestType int getRequestType() {
+ return mRequestParcel.requestType;
+ }
+
+ /**
* String of TetheringRequest detail.
* @hide
*/
@@ -1168,6 +1222,13 @@
public String toString() {
StringJoiner sj = new StringJoiner(", ", "TetheringRequest[ ", " ]");
sj.add(typeToString(mRequestParcel.tetheringType));
+ if (mRequestParcel.requestType == REQUEST_TYPE_IMPLICIT) {
+ sj.add("IMPLICIT");
+ } else if (mRequestParcel.requestType == REQUEST_TYPE_LEGACY) {
+ sj.add("LEGACY");
+ } else if (mRequestParcel.requestType == REQUEST_TYPE_PLACEHOLDER) {
+ sj.add("PLACEHOLDER");
+ }
if (mRequestParcel.localIPv4Address != null) {
sj.add("localIpv4Address=" + mRequestParcel.localIPv4Address);
}
@@ -1217,7 +1278,8 @@
public boolean equalsIgnoreUidPackage(TetheringRequest otherRequest) {
TetheringRequestParcel parcel = getParcel();
TetheringRequestParcel otherParcel = otherRequest.getParcel();
- return parcel.tetheringType == otherParcel.tetheringType
+ return parcel.requestType == otherParcel.requestType
+ && parcel.tetheringType == otherParcel.tetheringType
&& Objects.equals(parcel.localIPv4Address, otherParcel.localIPv4Address)
&& Objects.equals(parcel.staticClientAddress, otherParcel.staticClientAddress)
&& parcel.exemptFromEntitlementCheck == otherParcel.exemptFromEntitlementCheck
@@ -1360,7 +1422,25 @@
@FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
public void stopTethering(@NonNull TetheringRequest request,
@NonNull final Executor executor, @NonNull final StopTetheringCallback callback) {
- throw new UnsupportedOperationException();
+ Objects.requireNonNull(request);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+
+ final String callerPkg = mContext.getOpPackageName();
+ Log.i(TAG, "stopTethering: request=" + request + ", caller=" + callerPkg);
+ getConnector(c -> c.stopTetheringRequest(request, callerPkg, getAttributionTag(),
+ new IIntResultListener.Stub() {
+ @Override
+ public void onResult(final int resultCode) {
+ executor.execute(() -> {
+ if (resultCode == TETHER_ERROR_NO_ERROR) {
+ callback.onStopTetheringSucceeded();
+ } else {
+ callback.onStopTetheringFailed(resultCode);
+ }
+ });
+ }
+ }));
}
/**
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl b/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
index 97c9b9a..9863385 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
@@ -24,6 +24,7 @@
* @hide
*/
parcelable TetheringRequestParcel {
+ int requestType;
int tetheringType;
LinkAddress localIPv4Address;
LinkAddress staticClientAddress;
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index 6229f6d..d0ba431 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -429,9 +429,11 @@
return Collections.unmodifiableList(mDhcpLeases);
}
- /** Enable this IpServer. IpServer state machine will be tethered or localHotspot state. */
- public void enable(final int requestedState, final TetheringRequest request) {
- sendMessage(CMD_TETHER_REQUESTED, requestedState, 0, request);
+ /**
+ * Enable this IpServer. IpServer state machine will be tethered or localHotspot state based on
+ * the connectivity scope of the TetheringRequest. */
+ public void enable(@NonNull final TetheringRequest request) {
+ sendMessage(CMD_TETHER_REQUESTED, 0, 0, request);
}
/** Stop this IpServer. After this is called this IpServer should not be used any more. */
@@ -850,16 +852,13 @@
}
}
- private void removeRoutesFromNetworkAndLinkProperties(int netId,
- @NonNull final List<RouteInfo> toBeRemoved) {
+ private void removeRoutesFromNetwork(int netId, @NonNull final List<RouteInfo> toBeRemoved) {
final int removalFailures = NetdUtils.removeRoutesFromNetwork(
mNetd, netId, toBeRemoved);
if (removalFailures > 0) {
mLog.e("Failed to remove " + removalFailures
+ " IPv6 routes from network " + netId + ".");
}
-
- for (RouteInfo route : toBeRemoved) mLinkProperties.removeRoute(route);
}
private void addInterfaceToNetwork(final int netId, @NonNull final String ifaceName) {
@@ -886,7 +885,7 @@
}
}
- private void addRoutesToNetworkAndLinkProperties(int netId,
+ private void addRoutesToNetwork(int netId,
@NonNull final List<RouteInfo> toBeAdded) {
// It's safe to call addInterfaceToNetwork() even if
// the interface is already in the network.
@@ -899,16 +898,16 @@
mLog.e("Failed to add IPv4/v6 routes to local table: " + e);
return;
}
-
- for (RouteInfo route : toBeAdded) mLinkProperties.addRoute(route);
}
private void configureLocalIPv6Routes(
ArraySet<IpPrefix> deprecatedPrefixes, ArraySet<IpPrefix> newPrefixes) {
// [1] Remove the routes that are deprecated.
if (!deprecatedPrefixes.isEmpty()) {
- removeRoutesFromNetworkAndLinkProperties(LOCAL_NET_ID,
- getLocalRoutesFor(mIfaceName, deprecatedPrefixes));
+ final List<RouteInfo> routesToBeRemoved =
+ getLocalRoutesFor(mIfaceName, deprecatedPrefixes);
+ removeRoutesFromNetwork(LOCAL_NET_ID, routesToBeRemoved);
+ for (RouteInfo route : routesToBeRemoved) mLinkProperties.removeRoute(route);
}
// [2] Add only the routes that have not previously been added.
@@ -919,8 +918,10 @@
}
if (!addedPrefixes.isEmpty()) {
- addRoutesToNetworkAndLinkProperties(LOCAL_NET_ID,
- getLocalRoutesFor(mIfaceName, addedPrefixes));
+ final List<RouteInfo> routesToBeAdded =
+ getLocalRoutesFor(mIfaceName, addedPrefixes);
+ addRoutesToNetwork(LOCAL_NET_ID, routesToBeAdded);
+ for (RouteInfo route : routesToBeAdded) mLinkProperties.addRoute(route);
}
}
}
@@ -1026,11 +1027,11 @@
mLinkProperties.setInterfaceName(mIfaceName);
}
- private void maybeConfigureStaticIp(final TetheringRequest request) {
+ private void maybeConfigureStaticIp(@NonNull final TetheringRequest request) {
// Ignore static address configuration if they are invalid or null. In theory, static
// addresses should not be invalid here because TetheringManager do not allow caller to
// specify invalid static address configuration.
- if (request == null || request.getLocalIpv4Address() == null
+ if (request.getLocalIpv4Address() == null
|| request.getClientStaticIpv4Address() == null || !checkStaticAddressConfiguration(
request.getLocalIpv4Address(), request.getClientStaticIpv4Address())) {
return;
@@ -1053,13 +1054,13 @@
case CMD_TETHER_REQUESTED:
mLastError = TETHER_ERROR_NO_ERROR;
mTetheringRequest = (TetheringRequest) message.obj;
- switch (message.arg1) {
- case STATE_LOCAL_ONLY:
- maybeConfigureStaticIp((TetheringRequest) message.obj);
+ switch (mTetheringRequest.getConnectivityScope()) {
+ case CONNECTIVITY_SCOPE_LOCAL:
+ maybeConfigureStaticIp(mTetheringRequest);
transitionTo(mLocalHotspotState);
break;
- case STATE_TETHERED:
- maybeConfigureStaticIp((TetheringRequest) message.obj);
+ case CONNECTIVITY_SCOPE_GLOBAL:
+ maybeConfigureStaticIp(mTetheringRequest);
transitionTo(mTetheredState);
break;
default:
@@ -1124,8 +1125,17 @@
}
try {
- NetdUtils.tetherInterface(mNetd, LOCAL_NET_ID, mIfaceName,
- asIpPrefix(mIpv4Address));
+ // Enable IPv6, disable accepting RA, etc. See TetherController::tetherInterface()
+ // for more detail.
+ mNetd.tetherInterfaceAdd(mIfaceName);
+ NetdUtils.networkAddInterface(mNetd, LOCAL_NET_ID, mIfaceName,
+ 20 /* maxAttempts */, 50 /* pollingIntervalMs */);
+ // Activate a route to dest and IPv6 link local.
+ NetdUtils.modifyRoute(mNetd, NetdUtils.ModifyOperation.ADD, LOCAL_NET_ID,
+ new RouteInfo(asIpPrefix(mIpv4Address), null, mIfaceName, RTN_UNICAST));
+ NetdUtils.modifyRoute(mNetd, NetdUtils.ModifyOperation.ADD, LOCAL_NET_ID,
+ new RouteInfo(new IpPrefix("fe80::/64"), null, mIfaceName,
+ RTN_UNICAST));
} catch (RemoteException | ServiceSpecificException | IllegalStateException e) {
mLog.e("Error Tethering", e);
mLastError = TETHER_ERROR_TETHER_IFACE_ERROR;
@@ -1146,8 +1156,13 @@
// all in sequence.
stopIPv6();
+ // Reset interface for tethering.
try {
- NetdUtils.untetherInterface(mNetd, LOCAL_NET_ID, mIfaceName);
+ try {
+ mNetd.tetherInterfaceRemove(mIfaceName);
+ } finally {
+ mNetd.networkRemoveInterface(LOCAL_NET_ID, mIfaceName);
+ }
} catch (RemoteException | ServiceSpecificException e) {
mLastError = TETHER_ERROR_UNTETHER_IFACE_ERROR;
mLog.e("Failed to untether interface: " + e);
@@ -1225,13 +1240,16 @@
}
// Remove deprecated routes from downstream network.
- removeRoutesFromNetworkAndLinkProperties(LOCAL_NET_ID,
- List.of(getDirectConnectedRoute(deprecatedLinkAddress)));
+ final List<RouteInfo> routesToBeRemoved =
+ List.of(getDirectConnectedRoute(deprecatedLinkAddress));
+ removeRoutesFromNetwork(LOCAL_NET_ID, routesToBeRemoved);
+ for (RouteInfo route : routesToBeRemoved) mLinkProperties.removeRoute(route);
mLinkProperties.removeLinkAddress(deprecatedLinkAddress);
// Add new routes to downstream network.
- addRoutesToNetworkAndLinkProperties(LOCAL_NET_ID,
- List.of(getDirectConnectedRoute(mIpv4Address)));
+ final List<RouteInfo> routesToBeAdded = List.of(getDirectConnectedRoute(mIpv4Address));
+ addRoutesToNetwork(LOCAL_NET_ID, routesToBeAdded);
+ for (RouteInfo route : routesToBeAdded) mLinkProperties.addRoute(route);
mLinkProperties.addLinkAddress(mIpv4Address);
// Update local DNS caching server with new IPv4 address, otherwise, dnsmasq doesn't
diff --git a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
index a942166..900b505 100644
--- a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
+++ b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
@@ -62,6 +62,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.FrameworkConnectivityStatsLog;
import com.android.net.module.util.SharedLog;
import java.io.PrintWriter;
@@ -154,14 +155,27 @@
// Only launch entitlement UI for the current user if it is allowed to
// change tethering. This usually means the system user or the admin users in HSUM.
- // TODO (b/382624069): Figure out whether it is safe to call createContextAsUser
- // from secondary user. And re-enable the check or remove the code accordingly.
- if (false) {
+ if (SdkLevel.isAtLeastT()) {
// Create a user context for the current foreground user as UserManager#isAdmin()
// operates on the context user.
final int currentUserId = getCurrentUser();
final UserHandle currentUser = UserHandle.of(currentUserId);
- final Context userContext = mContext.createContextAsUser(currentUser, 0);
+ final Context userContext;
+ try {
+ // There is no safe way to invoke this method since tethering package
+ // might not be installed for a certain user on the OEM devices,
+ // refer to b/382628161.
+ userContext = mContext.createContextAsUser(currentUser, 0);
+ } catch (IllegalStateException e) {
+ FrameworkConnectivityStatsLog.write(
+ FrameworkConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+ FrameworkConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_ENTITLEMENT_CREATE_CONTEXT_AS_USER_THROWS
+ );
+ // Fallback to startActivity if createContextAsUser failed.
+ mLog.e("createContextAsUser failed, fallback to startActivity", e);
+ mContext.startActivity(intent);
+ return intent;
+ }
final UserManager userManager = userContext.getSystemService(UserManager.class);
if (userManager.isAdminUser()) {
diff --git a/Tethering/src/com/android/networkstack/tethering/RequestTracker.java b/Tethering/src/com/android/networkstack/tethering/RequestTracker.java
new file mode 100644
index 0000000..3ebe4f7
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/RequestTracker.java
@@ -0,0 +1,164 @@
+/*
+ * 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.networkstack.tethering;
+
+import static com.android.networkstack.tethering.util.TetheringUtils.createPlaceholderRequest;
+
+import android.net.TetheringManager.TetheringRequest;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Helper class to keep track of tethering requests.
+ * The intended usage of this class is
+ * 1) Add a pending request with {@link #addPendingRequest(TetheringRequest)} before asking the link
+ * layer to start.
+ * 2) When the link layer is up, use {@link #getOrCreatePendingRequest(int)} to get a request to
+ * start IP serving with.
+ * 3) Remove all pending requests with {@link #removeAllPendingRequests(int)}.
+ * Note: This class is not thread-safe.
+ * TODO: Add the pending IIntResultListeners at the same time as the pending requests, and
+ * call them when we get the tether result.
+ * TODO: Add support for multiple Bluetooth requests before the PAN service connects instead of
+ * using a separate mPendingPanRequestListeners.
+ * TODO: Add support for fuzzy-matched requests.
+ */
+public class RequestTracker {
+ private static final String TAG = RequestTracker.class.getSimpleName();
+
+ private class PendingRequest {
+ @NonNull
+ private final TetheringRequest mTetheringRequest;
+
+ private PendingRequest(@NonNull TetheringRequest tetheringRequest) {
+ mTetheringRequest = tetheringRequest;
+ }
+
+ @NonNull
+ TetheringRequest getTetheringRequest() {
+ return mTetheringRequest;
+ }
+ }
+
+ public enum AddResult {
+ /**
+ * Request was successfully added
+ */
+ SUCCESS,
+ /**
+ * Failure indicating that the request could not be added due to a request of the same type
+ * with conflicting parameters already pending. If so, we must stop tethering for the
+ * pending request before trying to add the result again.
+ */
+ FAILURE_CONFLICTING_PENDING_REQUEST
+ }
+
+ /**
+ * List of pending requests added by {@link #addPendingRequest(TetheringRequest)}. There can be
+ * only one per type, since we remove every request of the same type when we add a request.
+ */
+ private final List<PendingRequest> mPendingRequests = new ArrayList<>();
+
+ @VisibleForTesting
+ List<TetheringRequest> getPendingTetheringRequests() {
+ List<TetheringRequest> requests = new ArrayList<>();
+ for (PendingRequest pendingRequest : mPendingRequests) {
+ requests.add(pendingRequest.getTetheringRequest());
+ }
+ return requests;
+ }
+
+ /**
+ * Add a pending request and listener. The request should be added before asking the link layer
+ * to start, and should be retrieved with {@link #getNextPendingRequest(int)} once the link
+ * layer comes up. The result of the add operation will be returned as an AddResult code.
+ */
+ public AddResult addPendingRequest(@NonNull final TetheringRequest newRequest) {
+ // Check the existing requests to see if it is OK to add the new request.
+ for (PendingRequest request : mPendingRequests) {
+ TetheringRequest existingRequest = request.getTetheringRequest();
+ if (existingRequest.getTetheringType() != newRequest.getTetheringType()) {
+ continue;
+ }
+
+ // Can't add request if there's a request of the same type with different
+ // parameters.
+ if (!existingRequest.equalsIgnoreUidPackage(newRequest)) {
+ return AddResult.FAILURE_CONFLICTING_PENDING_REQUEST;
+ }
+ }
+
+ // Remove the existing pending request of the same type. We already filter out for
+ // conflicting parameters above, so these would have been equivalent anyway (except for
+ // UID).
+ removeAllPendingRequests(newRequest.getTetheringType());
+ mPendingRequests.add(new PendingRequest(newRequest));
+ return AddResult.SUCCESS;
+ }
+
+ /**
+ * Gets the next pending TetheringRequest of a given type, or creates a placeholder request if
+ * there are none.
+ * Note: There are edge cases where the pending request is absent and we must temporarily
+ * synthesize a placeholder request, such as if stopTethering was called before link
+ * layer went up, or if the link layer goes up without us poking it (e.g. adb shell
+ * cmd wifi start-softap). These placeholder requests only specify the tethering type
+ * and the default connectivity scope.
+ */
+ @NonNull
+ public TetheringRequest getOrCreatePendingRequest(int type) {
+ TetheringRequest pending = getNextPendingRequest(type);
+ if (pending != null) return pending;
+
+ Log.w(TAG, "No pending TetheringRequest for type " + type + " found, creating a"
+ + " placeholder request");
+ return createPlaceholderRequest(type);
+ }
+
+ /**
+ * Same as {@link #getOrCreatePendingRequest(int)} but returns {@code null} if there's no
+ * pending request found.
+ *
+ * @param type Tethering type of the pending request
+ * @return pending request or {@code null} if there are none.
+ */
+ @Nullable
+ public TetheringRequest getNextPendingRequest(int type) {
+ for (PendingRequest pendingRequest : mPendingRequests) {
+ TetheringRequest tetheringRequest =
+ pendingRequest.getTetheringRequest();
+ if (tetheringRequest.getTetheringType() == type) return tetheringRequest;
+ }
+ return null;
+ }
+
+ /**
+ * Removes all pending requests of the given tethering type.
+ *
+ * @param type Tethering type
+ */
+ public void removeAllPendingRequests(int type) {
+ mPendingRequests.removeIf(r -> r.getTetheringRequest().getTetheringType() == type);
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index a388624..1589509 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -28,7 +28,7 @@
import static android.net.ConnectivityManager.EXTRA_NETWORK_INFO;
import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
import static android.net.TetheringManager.ACTION_TETHER_STATE_CHANGED;
-import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
import static android.net.TetheringManager.EXTRA_ACTIVE_LOCAL_ONLY;
import static android.net.TetheringManager.EXTRA_ACTIVE_TETHER;
import static android.net.TetheringManager.EXTRA_AVAILABLE_TETHER;
@@ -42,15 +42,18 @@
import static android.net.TetheringManager.TETHERING_WIFI;
import static android.net.TetheringManager.TETHERING_WIFI_P2P;
import static android.net.TetheringManager.TETHERING_WIGIG;
+import static android.net.TetheringManager.TETHER_ERROR_BLUETOOTH_SERVICE_PENDING;
import static android.net.TetheringManager.TETHER_ERROR_INTERNAL_ERROR;
import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
import static android.net.TetheringManager.TETHER_ERROR_SERVICE_UNAVAIL;
import static android.net.TetheringManager.TETHER_ERROR_UNAVAIL_IFACE;
import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_IFACE;
+import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_REQUEST;
import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_TYPE;
import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_FAILED;
import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STARTED;
import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STOPPED;
+import static android.net.TetheringManager.TetheringRequest.REQUEST_TYPE_PLACEHOLDER;
import static android.net.TetheringManager.toIfaces;
import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_INTERFACE_NAME;
import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_MODE;
@@ -72,7 +75,11 @@
import static com.android.networkstack.tethering.metrics.TetheringStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI_P2P;
import static com.android.networkstack.tethering.metrics.TetheringStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI_P2P_SUCCESS;
import static com.android.networkstack.tethering.metrics.TetheringStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI_SUCCESS;
+import static com.android.networkstack.tethering.metrics.TetheringStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_TETHER_WITH_PLACEHOLDER_REQUEST;
import static com.android.networkstack.tethering.util.TetheringMessageBase.BASE_MAIN_SM;
+import static com.android.networkstack.tethering.util.TetheringUtils.createImplicitLocalOnlyTetheringRequest;
+import static com.android.networkstack.tethering.util.TetheringUtils.createLegacyGlobalScopeTetheringRequest;
+import static com.android.networkstack.tethering.util.TetheringUtils.createPlaceholderRequest;
import android.app.usage.NetworkStatsManager;
import android.bluetooth.BluetoothAdapter;
@@ -128,7 +135,6 @@
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
-import android.util.Pair;
import android.util.SparseArray;
import androidx.annotation.NonNull;
@@ -237,11 +243,7 @@
private final SharedLog mLog = new SharedLog(TAG);
private final RemoteCallbackList<ITetheringEventCallback> mTetheringEventCallbacks =
new RemoteCallbackList<>();
- // Currently active tethering requests per tethering type. Only one of each type can be
- // requested at a time. After a tethering type is requested, the map keeps tethering parameters
- // to be used after the interface comes up asynchronously.
- private final SparseArray<TetheringRequest> mActiveTetheringRequests =
- new SparseArray<>();
+ private final RequestTracker mRequestTracker;
private final Context mContext;
private final ArrayMap<String, TetherState> mTetherStates;
@@ -288,7 +290,7 @@
private SettingsObserver mSettingsObserver;
private BluetoothPan mBluetoothPan;
private PanServiceListener mBluetoothPanListener;
- private ArrayList<Pair<Boolean, IIntResultListener>> mPendingPanRequests;
+ private final ArrayList<IIntResultListener> mPendingPanRequestListeners;
// AIDL doesn't support Set<Integer>. Maintain a int bitmap here. When the bitmap is passed to
// TetheringManager, TetheringManager would convert it to a set of Integer types.
// mSupportedTypeBitmap should always be updated inside tethering internal thread but it may be
@@ -304,11 +306,12 @@
mLooper = mDeps.makeTetheringLooper();
mNotificationUpdater = mDeps.makeNotificationUpdater(mContext, mLooper);
mTetheringMetrics = mDeps.makeTetheringMetrics(mContext);
+ mRequestTracker = new RequestTracker();
// This is intended to ensrure that if something calls startTethering(bluetooth) just after
// bluetooth is enabled. Before onServiceConnected is called, store the calls into this
// list and handle them as soon as onServiceConnected is called.
- mPendingPanRequests = new ArrayList<>();
+ mPendingPanRequestListeners = new ArrayList<>();
mTetherStates = new ArrayMap<>();
mConnectedClientsTracker = new ConnectedClientsTracker();
@@ -639,7 +642,7 @@
}
if (enabled) {
- ensureIpServerStarted(iface);
+ ensureIpServerStartedForInterface(iface);
} else {
ensureIpServerStopped(iface);
}
@@ -700,15 +703,13 @@
final IIntResultListener listener) {
mHandler.post(() -> {
final int type = request.getTetheringType();
- final TetheringRequest unfinishedRequest = mActiveTetheringRequests.get(type);
- // If tethering is already enabled with a different request,
- // disable before re-enabling.
- if (unfinishedRequest != null && !unfinishedRequest.equalsIgnoreUidPackage(request)) {
- enableTetheringInternal(type, false /* disabled */,
- unfinishedRequest.getInterfaceName(), null);
- mEntitlementMgr.stopProvisioningIfNeeded(type);
+ RequestTracker.AddResult result = mRequestTracker.addPendingRequest(request);
+ // If tethering is already pending with a conflicting request, stop tethering before
+ // starting.
+ if (result == RequestTracker.AddResult.FAILURE_CONFLICTING_PENDING_REQUEST) {
+ stopTetheringInternal(type); // Also removes the request from the tracker.
+ mRequestTracker.addPendingRequest(request);
}
- mActiveTetheringRequests.put(type, request);
if (request.isExemptFromEntitlementCheck()) {
mEntitlementMgr.setExemptedDownstreamType(type);
@@ -716,7 +717,7 @@
mEntitlementMgr.startProvisioningIfNeeded(type,
request.getShouldShowEntitlementUi());
}
- enableTetheringInternal(type, true /* enabled */, request.getInterfaceName(), listener);
+ enableTetheringInternal(true /* enabled */, request, listener);
mTetheringMetrics.createBuilder(type, callerPkg);
});
}
@@ -726,10 +727,45 @@
stopTetheringInternal(type);
});
}
- void stopTetheringInternal(int type) {
- mActiveTetheringRequests.remove(type);
- enableTetheringInternal(type, false /* disabled */, null, null);
+ private boolean isTetheringTypePendingOrServing(final int type) {
+ if (mRequestTracker.getNextPendingRequest(type) != null) return true;
+ for (TetherState state : mTetherStates.values()) {
+ // TODO: isCurrentlyServing only starts returning true once the IpServer has processed
+ // the CMD_TETHER_REQUESTED. Ensure that we consider the request to be serving even when
+ // that has not happened yet.
+ if (state.isCurrentlyServing() && state.ipServer.interfaceType() == type) return true;
+ }
+ return false;
+ }
+
+ void stopTetheringRequest(@NonNull final TetheringRequest request,
+ @NonNull final IIntResultListener listener) {
+ if (!isTetheringWithSoftApConfigEnabled()) return;
+ mHandler.post(() -> {
+ final int type = request.getTetheringType();
+ if (isTetheringTypePendingOrServing(type)) {
+ stopTetheringInternal(type);
+ try {
+ listener.onResult(TETHER_ERROR_NO_ERROR);
+ } catch (RemoteException ignored) { }
+ return;
+ }
+
+ // Request doesn't match any active requests, ignore.
+ try {
+ listener.onResult(TETHER_ERROR_UNKNOWN_REQUEST);
+ } catch (RemoteException ignored) { }
+ });
+ }
+
+ void stopTetheringInternal(int type) {
+ mRequestTracker.removeAllPendingRequests(type);
+
+ // Using a placeholder here is ok since none of the disable APIs use the request for
+ // anything. We simply need the tethering type to know which link layer to poke for removal.
+ // TODO: Remove the placeholder here and loop through each pending/serving request.
+ enableTetheringInternal(false /* disabled */, createPlaceholderRequest(type), null);
mEntitlementMgr.stopProvisioningIfNeeded(type);
}
@@ -737,9 +773,10 @@
* Enables or disables tethering for the given type. If provisioning is required, it will
* schedule provisioning rechecks for the specified interface.
*/
- private void enableTetheringInternal(int type, boolean enable,
- String iface, final IIntResultListener listener) {
- int result = TETHER_ERROR_NO_ERROR;
+ private void enableTetheringInternal(boolean enable, @NonNull final TetheringRequest request,
+ final IIntResultListener listener) {
+ final int type = request.getTetheringType();
+ final int result;
switch (type) {
case TETHERING_WIFI:
result = setWifiTethering(enable);
@@ -748,7 +785,7 @@
result = setUsbTethering(enable);
break;
case TETHERING_BLUETOOTH:
- setBluetoothTethering(enable, listener);
+ result = setBluetoothTethering(enable, listener);
break;
case TETHERING_NCM:
result = setNcmTethering(enable);
@@ -757,17 +794,17 @@
result = setEthernetTethering(enable);
break;
case TETHERING_VIRTUAL:
- result = setVirtualMachineTethering(enable, iface);
+ result = setVirtualMachineTethering(enable, request);
break;
default:
Log.w(TAG, "Invalid tether type.");
result = TETHER_ERROR_UNKNOWN_TYPE;
}
- // The result of Bluetooth tethering will be sent by #setBluetoothTethering.
- if (type != TETHERING_BLUETOOTH) {
- sendTetherResult(listener, result, type);
- }
+ // The result of Bluetooth tethering will be sent after the pan service connects.
+ if (result == TETHER_ERROR_BLUETOOTH_SERVICE_PENDING) return;
+
+ sendTetherResult(listener, result, type);
}
private void sendTetherResult(final IIntResultListener listener, final int result,
@@ -781,7 +818,7 @@
// If changing tethering fail, remove corresponding request
// no matter who trigger the start/stop.
if (result != TETHER_ERROR_NO_ERROR) {
- mActiveTetheringRequests.remove(type);
+ mRequestTracker.removeAllPendingRequests(type);
mTetheringMetrics.updateErrorCode(type, result);
mTetheringMetrics.sendReport(type);
}
@@ -806,13 +843,12 @@
return TETHER_ERROR_INTERNAL_ERROR;
}
- private void setBluetoothTethering(final boolean enable, final IIntResultListener listener) {
+ private int setBluetoothTethering(final boolean enable, final IIntResultListener listener) {
final BluetoothAdapter adapter = mDeps.getBluetoothAdapter();
if (adapter == null || !adapter.isEnabled()) {
Log.w(TAG, "Tried to enable bluetooth tethering with null or disabled adapter. null: "
+ (adapter == null));
- sendTetherResult(listener, TETHER_ERROR_SERVICE_UNAVAIL, TETHERING_BLUETOOTH);
- return;
+ return TETHER_ERROR_SERVICE_UNAVAIL;
}
if (mBluetoothPanListener != null && mBluetoothPanListener.isConnected()) {
@@ -820,16 +856,21 @@
// When bluetooth tethering is enabled, any time a PAN client pairs with this
// host, bluetooth will bring up a bt-pan interface and notify tethering to
// enable IP serving.
- setBluetoothTetheringSettings(mBluetoothPan, enable, listener);
- return;
+ return setBluetoothTetheringSettings(mBluetoothPan, enable);
}
- // The reference of IIntResultListener should only exist when application want to start
- // tethering but tethering is not bound to pan service yet. Even if the calling process
- // dies, the referenice of IIntResultListener would still keep in mPendingPanRequests. Once
- // tethering bound to pan service (onServiceConnected) or bluetooth just crash
- // (onServiceDisconnected), all the references from mPendingPanRequests would be cleared.
- mPendingPanRequests.add(new Pair(enable, listener));
+ if (!enable) {
+ // The service is not connected. If disabling tethering, there's no point starting
+ // the service just to stop tethering since tethering is not started. Just remove
+ // any pending requests to enable tethering, and notify them that they have failed.
+ for (IIntResultListener pendingListener : mPendingPanRequestListeners) {
+ sendTetherResult(pendingListener, TETHER_ERROR_SERVICE_UNAVAIL,
+ TETHERING_BLUETOOTH);
+ }
+ mPendingPanRequestListeners.clear();
+ return TETHER_ERROR_NO_ERROR;
+ }
+ mPendingPanRequestListeners.add(listener);
// Bluetooth tethering is not a popular feature. To avoid bind to bluetooth pan service all
// the time but user never use bluetooth tethering. mBluetoothPanListener is created first
@@ -839,6 +880,7 @@
mBluetoothPanListener = new PanServiceListener();
adapter.getProfileProxy(mContext, mBluetoothPanListener, BluetoothProfile.PAN);
}
+ return TETHER_ERROR_BLUETOOTH_SERVICE_PENDING;
}
private class PanServiceListener implements ServiceListener {
@@ -855,10 +897,12 @@
mBluetoothPan = (BluetoothPan) proxy;
mIsConnected = true;
- for (Pair<Boolean, IIntResultListener> request : mPendingPanRequests) {
- setBluetoothTetheringSettings(mBluetoothPan, request.first, request.second);
+ for (IIntResultListener pendingListener : mPendingPanRequestListeners) {
+ final int result = setBluetoothTetheringSettings(mBluetoothPan,
+ true /* enable */);
+ sendTetherResult(pendingListener, result, TETHERING_BLUETOOTH);
}
- mPendingPanRequests.clear();
+ mPendingPanRequestListeners.clear();
});
}
@@ -869,11 +913,11 @@
// reachable before next onServiceConnected.
mIsConnected = false;
- for (Pair<Boolean, IIntResultListener> request : mPendingPanRequests) {
- sendTetherResult(request.second, TETHER_ERROR_SERVICE_UNAVAIL,
+ for (IIntResultListener pendingListener : mPendingPanRequestListeners) {
+ sendTetherResult(pendingListener, TETHER_ERROR_SERVICE_UNAVAIL,
TETHERING_BLUETOOTH);
}
- mPendingPanRequests.clear();
+ mPendingPanRequestListeners.clear();
mBluetoothIfaceRequest = null;
mBluetoothCallback = null;
maybeDisableBluetoothIpServing();
@@ -885,8 +929,8 @@
}
}
- private void setBluetoothTetheringSettings(@NonNull final BluetoothPan bluetoothPan,
- final boolean enable, final IIntResultListener listener) {
+ private int setBluetoothTetheringSettings(@NonNull final BluetoothPan bluetoothPan,
+ final boolean enable) {
if (SdkLevel.isAtLeastT()) {
changeBluetoothTetheringSettings(bluetoothPan, enable);
} else {
@@ -895,9 +939,8 @@
// Enabling bluetooth tethering settings can silently fail. Send internal error if the
// result is not expected.
- final int result = bluetoothPan.isTetheringOn() == enable
+ return bluetoothPan.isTetheringOn() == enable
? TETHER_ERROR_NO_ERROR : TETHER_ERROR_INTERNAL_ERROR;
- sendTetherResult(listener, result, TETHERING_BLUETOOTH);
}
private void changeBluetoothTetheringSettingsPreT(@NonNull final BluetoothPan bluetoothPan,
@@ -943,7 +986,9 @@
public void onAvailable(String iface) {
if (this != mBluetoothCallback) return;
- enableIpServing(TETHERING_BLUETOOTH, iface, getRequestedState(TETHERING_BLUETOOTH));
+ final TetheringRequest request =
+ mRequestTracker.getOrCreatePendingRequest(TETHERING_BLUETOOTH);
+ enableIpServing(request, iface);
mConfiguredBluetoothIface = iface;
}
@@ -998,7 +1043,10 @@
// Ethernet callback arrived after Ethernet tethering stopped. Ignore.
return;
}
- enableIpServing(TETHERING_ETHERNET, iface, getRequestedState(TETHERING_ETHERNET));
+
+ final TetheringRequest request = mRequestTracker.getOrCreatePendingRequest(
+ TETHERING_ETHERNET);
+ enableIpServing(request, iface);
mConfiguredEthernetIface = iface;
}
@@ -1012,17 +1060,16 @@
}
}
- private int setVirtualMachineTethering(final boolean enable, String iface) {
+ private int setVirtualMachineTethering(final boolean enable,
+ @NonNull final TetheringRequest request) {
+ final String iface = request.getInterfaceName();
if (enable) {
if (TextUtils.isEmpty(iface)) {
mConfiguredVirtualIface = "avf_tap_fixed";
} else {
mConfiguredVirtualIface = iface;
}
- enableIpServing(
- TETHERING_VIRTUAL,
- mConfiguredVirtualIface,
- getRequestedState(TETHERING_VIRTUAL));
+ enableIpServing(request, mConfiguredVirtualIface);
} else if (mConfiguredVirtualIface != null) {
ensureIpServerStopped(mConfiguredVirtualIface);
mConfiguredVirtualIface = null;
@@ -1030,6 +1077,62 @@
return TETHER_ERROR_NO_ERROR;
}
+ private void handleLegacyTether(String iface, final IIntResultListener listener) {
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ // After V, the TetheringManager and ConnectivityManager tether and untether methods
+ // throw UnsupportedOperationException, so this cannot happen in normal use. Ensure
+ // that this code cannot run even if callers use raw binder calls or other
+ // unsupported methods.
+ return;
+ }
+
+ final int type = ifaceNameToType(iface);
+ if (type == TETHERING_INVALID) {
+ Log.e(TAG, "Ignoring call to legacy tether for unknown iface " + iface);
+ try {
+ listener.onResult(TETHER_ERROR_UNKNOWN_IFACE);
+ } catch (RemoteException e) { }
+ }
+
+ TetheringRequest request = mRequestTracker.getNextPendingRequest(type);
+ if (request == null) {
+ request = createLegacyGlobalScopeTetheringRequest(type);
+ }
+ int result = tetherInternal(request, iface);
+ switch (type) {
+ case TETHERING_WIFI:
+ TerribleErrorLog.logTerribleError(TetheringStatsLog::write,
+ "Legacy tether API called on Wifi iface " + iface,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI);
+ if (result == TETHER_ERROR_NO_ERROR) {
+ TerribleErrorLog.logTerribleError(TetheringStatsLog::write,
+ "Legacy tether API succeeded on Wifi iface " + iface,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI_SUCCESS);
+ }
+ break;
+ case TETHERING_WIFI_P2P:
+ TerribleErrorLog.logTerribleError(TetheringStatsLog::write,
+ "Legacy tether API called on Wifi P2P iface " + iface,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI_P2P);
+ if (result == TETHER_ERROR_NO_ERROR) {
+ TerribleErrorLog.logTerribleError(TetheringStatsLog::write,
+ "Legacy tether API succeeded on Wifi P2P iface " + iface,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI_P2P_SUCCESS);
+ }
+ break;
+ default:
+ // Do nothing
+ break;
+ }
+ try {
+ listener.onResult(result);
+ } catch (RemoteException e) { }
+ }
+
/**
* Legacy tether API that starts tethering with CONNECTIVITY_SCOPE_GLOBAL on the given iface.
*
@@ -1043,52 +1146,11 @@
* WIFI_(AP/P2P_STATE_CHANGED broadcasts, which makes this API redundant for those types unless
* those broadcasts are disabled by OEM.
*/
- void tether(String iface, int requestedState, final IIntResultListener listener) {
- if (Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM) {
- // After V, the TetheringManager and ConnectivityManager tether and untether methods
- // throw UnsupportedOperationException, so this cannot happen in normal use. Ensure
- // that this code cannot run even if callers use raw binder calls or other
- // unsupported methods.
- return;
- }
- mHandler.post(() -> {
- int result = tether(iface, requestedState);
- switch (ifaceNameToType(iface)) {
- case TETHERING_WIFI:
- TerribleErrorLog.logTerribleError(TetheringStatsLog::write,
- "Legacy tether API called on Wifi iface " + iface,
- CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
- CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI);
- if (result == TETHER_ERROR_NO_ERROR) {
- TerribleErrorLog.logTerribleError(TetheringStatsLog::write,
- "Legacy tether API succeeded on Wifi iface " + iface,
- CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
- CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI_SUCCESS);
- }
- break;
- case TETHERING_WIFI_P2P:
- TerribleErrorLog.logTerribleError(TetheringStatsLog::write,
- "Legacy tether API called on Wifi P2P iface " + iface,
- CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
- CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI_P2P);
- if (result == TETHER_ERROR_NO_ERROR) {
- TerribleErrorLog.logTerribleError(TetheringStatsLog::write,
- "Legacy tether API succeeded on Wifi P2P iface " + iface,
- CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
- CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI_P2P_SUCCESS);
- }
- break;
- default:
- // Do nothing
- break;
- }
- try {
- listener.onResult(result);
- } catch (RemoteException e) { }
- });
+ void legacyTether(String iface, final IIntResultListener listener) {
+ mHandler.post(() -> handleLegacyTether(iface, listener));
}
- private int tether(String iface, int requestedState) {
+ private int tetherInternal(@NonNull TetheringRequest request, String iface) {
if (DBG) Log.d(TAG, "Tethering " + iface);
TetherState tetherState = mTetherStates.get(iface);
if (tetherState == null) {
@@ -1101,20 +1163,22 @@
Log.e(TAG, "Tried to Tether an unavailable iface: " + iface + ", ignoring");
return TETHER_ERROR_UNAVAIL_IFACE;
}
- // NOTE: If a CMD_TETHER_REQUESTED message is already in the TISM's queue but not yet
+ // NOTE: If a CMD_TETHER_REQUESTED message is already in the IpServer's queue but not yet
// processed, this will be a no-op and it will not return an error.
//
// This code cannot race with untether() because they both run on the handler thread.
- final int type = tetherState.ipServer.interfaceType();
- final TetheringRequest request = mActiveTetheringRequests.get(type, null);
- if (request != null) {
- mActiveTetheringRequests.delete(type);
+ mRequestTracker.removeAllPendingRequests(request.getTetheringType());
+ tetherState.ipServer.enable(request);
+ if (request.getRequestType() == REQUEST_TYPE_PLACEHOLDER) {
+ TerribleErrorLog.logTerribleError(TetheringStatsLog::write,
+ "Started tethering with placeholder request: " + request,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_TETHER_WITH_PLACEHOLDER_REQUEST);
}
- tetherState.ipServer.enable(requestedState, request);
return TETHER_ERROR_NO_ERROR;
}
- void untether(String iface, final IIntResultListener listener) {
+ void legacyUntether(String iface, final IIntResultListener listener) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM) {
// After V, the TetheringManager and ConnectivityManager tether and untether methods
// throw UnsupportedOperationException, so this cannot happen in normal use. Ensure
@@ -1124,13 +1188,13 @@
}
mHandler.post(() -> {
try {
- listener.onResult(untether(iface));
+ listener.onResult(legacyUntetherInternal(iface));
} catch (RemoteException e) {
}
});
}
- int untether(String iface) {
+ int legacyUntetherInternal(String iface) {
if (DBG) Log.d(TAG, "Untethering " + iface);
TetherState tetherState = mTetherStates.get(iface);
if (tetherState == null) {
@@ -1145,7 +1209,7 @@
return TETHER_ERROR_NO_ERROR;
}
- void untetherAll() {
+ void stopAllTethering() {
stopTethering(TETHERING_WIFI);
stopTethering(TETHERING_WIFI_P2P);
stopTethering(TETHERING_USB);
@@ -1169,22 +1233,6 @@
return mEntitlementMgr.isTetherProvisioningRequired(cfg);
}
- private int getRequestedState(int type) {
- final TetheringRequest request = mActiveTetheringRequests.get(type);
-
- // The request could have been deleted before we had a chance to complete it.
- // If so, assume that the scope is the default scope for this tethering type.
- // This likely doesn't matter - if the request has been deleted, then tethering is
- // likely going to be stopped soon anyway.
- final int connectivityScope = (request != null)
- ? request.getConnectivityScope()
- : TetheringRequest.getDefaultConnectivityScope(type);
-
- return connectivityScope == CONNECTIVITY_SCOPE_LOCAL
- ? IpServer.STATE_LOCAL_ONLY
- : IpServer.STATE_TETHERED;
- }
-
private int getServedUsbType(boolean forNcmFunction) {
// TETHERING_NCM is only used if the device does not use NCM for regular USB tethering.
if (forNcmFunction && !mConfig.isUsingNcm()) return TETHERING_NCM;
@@ -1315,7 +1363,7 @@
mLog.log("OBSERVED data saver changed");
handleDataSaverChanged();
} else if (action.equals(TetheringNotificationUpdater.ACTION_DISABLE_TETHERING)) {
- untetherAll();
+ stopAllTethering();
}
}
@@ -1479,14 +1527,14 @@
mDataSaverEnabled = isDataSaverEnabled;
if (mDataSaverEnabled) {
- untetherAll();
+ stopAllTethering();
}
}
}
@VisibleForTesting
- SparseArray<TetheringRequest> getActiveTetheringRequests() {
- return mActiveTetheringRequests;
+ List<TetheringRequest> getPendingTetheringRequests() {
+ return mRequestTracker.getPendingTetheringRequests();
}
@VisibleForTesting
@@ -1538,7 +1586,7 @@
mNotificationUpdater.notifyTetheringDisabledByRestriction();
// Untether from all downstreams since tethering is disallowed.
- mTethering.untetherAll();
+ mTethering.stopAllTethering();
}
return true;
@@ -1546,14 +1594,13 @@
}
}
- private void enableIpServing(int tetheringType, String ifname, int ipServingMode) {
- enableIpServing(tetheringType, ifname, ipServingMode, false /* isNcm */);
+ private void enableIpServing(@NonNull TetheringRequest request, String ifname) {
+ enableIpServing(request, ifname, false /* isNcm */);
}
- private void enableIpServing(int tetheringType, String ifname, int ipServingMode,
- boolean isNcm) {
- ensureIpServerStarted(ifname, tetheringType, isNcm);
- if (tether(ifname, ipServingMode) != TETHER_ERROR_NO_ERROR) {
+ private void enableIpServing(@NonNull TetheringRequest request, String ifname, boolean isNcm) {
+ ensureIpServerStartedForType(ifname, request.getTetheringType(), isNcm);
+ if (tetherInternal(request, ifname) != TETHER_ERROR_NO_ERROR) {
Log.e(TAG, "unable start tethering on iface " + ifname);
}
}
@@ -1605,7 +1652,10 @@
mLog.e(ifname + " is not a tetherable iface, ignoring");
return;
}
- enableIpServing(type, ifname, IpServer.STATE_LOCAL_ONLY);
+ // No need to call getOrCreatePendingRequest. There can never be explicit requests for
+ // TETHERING_WIFI_P2P because enableTetheringInternal ignores that type.
+ final TetheringRequest request = createImplicitLocalOnlyTetheringRequest(type);
+ enableIpServing(request, ifname);
}
private void disableWifiP2pIpServingIfNeeded(String ifname) {
@@ -1615,17 +1665,34 @@
disableWifiIpServingCommon(TETHERING_WIFI_P2P, ifname);
}
+ // TODO: fold this in to enableWifiIpServing. We cannot do this at the moment because there
+ // are tests that send wifi AP broadcasts with a null interface. But if this can't happen on
+ // real devices, we should fix those tests to always pass in an interface.
+ private int maybeInferWifiTetheringType(String ifname) {
+ return SdkLevel.isAtLeastT() ? TETHERING_WIFI : ifaceNameToType(ifname);
+ }
+
private void enableWifiIpServing(String ifname, int wifiIpMode) {
mLog.log("request WiFi tethering - interface=" + ifname + " state=" + wifiIpMode);
// Map wifiIpMode values to IpServer.Callback serving states.
- final int ipServingMode;
+ TetheringRequest request;
+ final int type;
switch (wifiIpMode) {
case IFACE_IP_MODE_TETHERED:
- ipServingMode = IpServer.STATE_TETHERED;
+ type = maybeInferWifiTetheringType(ifname);
+ request = mRequestTracker.getOrCreatePendingRequest(type);
+ // Wifi requests will always have CONNECTIVITY_SCOPE_GLOBAL, because
+ // TetheringRequest.Builder will not allow callers to set CONNECTIVITY_SCOPE_LOCAL
+ // for TETHERING_WIFI. However, if maybeInferWifiTetheringType returns a non-Wifi
+ // type (which could happen on a pre-T implementation of Wi-Fi if the regexps are
+ // misconfigured), then force the connectivity scope to global in order to match the
+ // historical behavior.
+ request.getParcel().connectivityScope = CONNECTIVITY_SCOPE_GLOBAL;
break;
case IFACE_IP_MODE_LOCAL_ONLY:
- ipServingMode = IpServer.STATE_LOCAL_ONLY;
+ type = maybeInferWifiTetheringType(ifname);
+ request = createImplicitLocalOnlyTetheringRequest(type);
break;
default:
mLog.e("Cannot enable IP serving in unknown WiFi mode: " + wifiIpMode);
@@ -1634,14 +1701,13 @@
// After T, tethering always trust the iface pass by state change intent. This allow
// tethering to deprecate tetherable wifi regexs after T.
- final int type = SdkLevel.isAtLeastT() ? TETHERING_WIFI : ifaceNameToType(ifname);
if (!checkTetherableType(type)) {
mLog.e(ifname + " is not a tetherable iface, ignoring");
return;
}
if (!TextUtils.isEmpty(ifname)) {
- enableIpServing(type, ifname, ipServingMode);
+ enableIpServing(request, ifname);
} else {
mLog.e("Cannot enable IP serving on missing interface name");
}
@@ -1662,7 +1728,6 @@
// for both TETHERING_USB and TETHERING_NCM, so the local-only NCM interface will be
// stopped immediately.
final int tetheringType = getServedUsbType(forNcmFunction);
- final int requestedState = getRequestedState(tetheringType);
String[] ifaces = null;
try {
ifaces = mNetd.interfaceGetList();
@@ -1671,10 +1736,11 @@
return;
}
+ final TetheringRequest request = mRequestTracker.getOrCreatePendingRequest(tetheringType);
if (ifaces != null) {
for (String iface : ifaces) {
if (ifaceNameToType(iface) == tetheringType) {
- enableIpServing(tetheringType, iface, requestedState, forNcmFunction);
+ enableIpServing(request, iface, forNcmFunction);
return;
}
}
@@ -2303,9 +2369,14 @@
break;
}
case EVENT_REQUEST_CHANGE_DOWNSTREAM: {
- final int tetheringType = message.arg1;
+ final int type = message.arg1;
final Boolean enabled = (Boolean) message.obj;
- enableTetheringInternal(tetheringType, enabled, null, null);
+ // Using a placeholder here is ok since we just need to the type of
+ // tethering to poke the link layer. When the link layer comes up, we won't
+ // have a pending request to use, but this matches the historical behavior.
+ // TODO: Get the TetheringRequest from IpServer and make sure to put it in
+ // the pending list too.
+ enableTetheringInternal(enabled, createPlaceholderRequest(type), null);
break;
}
default:
@@ -2906,7 +2977,7 @@
return type != TETHERING_INVALID;
}
- private void ensureIpServerStarted(final String iface) {
+ private void ensureIpServerStartedForInterface(final String iface) {
// If we don't care about this type of interface, ignore.
final int interfaceType = ifaceNameToType(iface);
if (!checkTetherableType(interfaceType)) {
@@ -2915,10 +2986,11 @@
return;
}
- ensureIpServerStarted(iface, interfaceType, false /* isNcm */);
+ ensureIpServerStartedForType(iface, interfaceType, false /* isNcm */);
}
- private void ensureIpServerStarted(final String iface, int interfaceType, boolean isNcm) {
+ private void ensureIpServerStartedForType(final String iface, int interfaceType,
+ boolean isNcm) {
// If we have already started a TISM for this interface, skip.
if (mTetherStates.containsKey(iface)) {
mLog.log("active iface (" + iface + ") reported as added, ignoring");
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
index b3e9c1b..3c91a1b 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
@@ -130,9 +130,6 @@
public static final String TETHER_ENABLE_WEAR_TETHERING =
"tether_enable_wear_tethering";
- public static final String TETHER_FORCE_RANDOM_PREFIX_BASE_SELECTION =
- "tether_force_random_prefix_base_selection";
-
public static final String TETHER_ENABLE_SYNC_SM = "tether_enable_sync_sm";
/**
@@ -142,7 +139,7 @@
public static final int DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS = 5000;
/** A flag for using synchronous or asynchronous state machine. */
- public static boolean USE_SYNC_SM = false;
+ public static boolean USE_SYNC_SM = true;
/**
* A feature flag to control whether the active sessions metrics should be enabled.
@@ -195,6 +192,10 @@
return DeviceConfigUtils.isTetheringFeatureEnabled(context, name);
}
+ boolean isFeatureNotChickenedOut(@NonNull Context context, @NonNull String name) {
+ return DeviceConfigUtils.isTetheringFeatureNotChickenedOut(context, name);
+ }
+
boolean getDeviceConfigBoolean(@NonNull String namespace, @NonNull String name,
boolean defaultValue) {
return DeviceConfig.getBoolean(namespace, name, defaultValue);
@@ -394,7 +395,7 @@
* use the async state machine.
*/
public void readEnableSyncSM(final Context ctx) {
- USE_SYNC_SM = mDeps.isFeatureEnabled(ctx, TETHER_ENABLE_SYNC_SM);
+ USE_SYNC_SM = mDeps.isFeatureNotChickenedOut(ctx, TETHER_ENABLE_SYNC_SM);
}
/** Does the dumping.*/
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringService.java b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
index 0c44a38..b553f46 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringService.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
@@ -111,7 +111,7 @@
IIntResultListener listener) {
if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, listener)) return;
- mTethering.tether(iface, IpServer.STATE_TETHERED, listener);
+ mTethering.legacyTether(iface, listener);
}
@Override
@@ -119,7 +119,7 @@
IIntResultListener listener) {
if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, listener)) return;
- mTethering.untether(iface, listener);
+ mTethering.legacyUntether(iface, listener);
}
@Override
@@ -159,6 +159,18 @@
}
@Override
+ public void stopTetheringRequest(TetheringRequest request,
+ String callerPkg, String callingAttributionTag,
+ IIntResultListener listener) {
+ if (request == null) return;
+ if (listener == null) return;
+ if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, listener)) return;
+ request.setUid(getBinderCallingUid());
+ request.setPackageName(callerPkg);
+ mTethering.stopTetheringRequest(request, listener);
+ }
+
+ @Override
public void requestLatestTetheringEntitlementResult(int type, ResultReceiver receiver,
boolean showEntitlementUi, String callerPkg, String callingAttributionTag) {
if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, receiver)) return;
@@ -200,7 +212,7 @@
if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, listener)) return;
try {
- mTethering.untetherAll();
+ mTethering.stopAllTethering();
listener.onResult(TETHER_ERROR_NO_ERROR);
} catch (RemoteException e) { }
}
diff --git a/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java b/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java
index 76c2f0d..79e6e16 100644
--- a/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java
+++ b/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java
@@ -15,7 +15,11 @@
*/
package com.android.networkstack.tethering.util;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
+
import android.net.TetherStatsParcel;
+import android.net.TetheringManager.TetheringRequest;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -166,4 +170,41 @@
return null;
}
}
+
+ /**
+ * Create a legacy tethering request for calls to the legacy tether() API, which doesn't take an
+ * explicit request. These are always CONNECTIVITY_SCOPE_GLOBAL, per historical behavior.
+ */
+ @NonNull
+ public static TetheringRequest createLegacyGlobalScopeTetheringRequest(int type) {
+ final TetheringRequest request = new TetheringRequest.Builder(type).build();
+ request.getParcel().requestType = TetheringRequest.REQUEST_TYPE_LEGACY;
+ request.getParcel().connectivityScope = CONNECTIVITY_SCOPE_GLOBAL;
+ return request;
+ }
+
+ /**
+ * Create a local-only implicit tethering request. This is used for Wifi local-only hotspot and
+ * Wifi P2P, which start tethering based on the WIFI_(AP/P2P)_STATE_CHANGED broadcasts.
+ */
+ @NonNull
+ public static TetheringRequest createImplicitLocalOnlyTetheringRequest(int type) {
+ final TetheringRequest request = new TetheringRequest.Builder(type).build();
+ request.getParcel().requestType = TetheringRequest.REQUEST_TYPE_IMPLICIT;
+ request.getParcel().connectivityScope = CONNECTIVITY_SCOPE_LOCAL;
+ return request;
+ }
+
+ /**
+ * Create a placeholder request. This is used in case we try to find a pending request but there
+ * is none (e.g. stopTethering removed a pending request), or for cases where we only have the
+ * tethering type (e.g. stopTethering(int)).
+ */
+ @NonNull
+ public static TetheringRequest createPlaceholderRequest(int type) {
+ final TetheringRequest request = new TetheringRequest.Builder(type).build();
+ request.getParcel().requestType = TetheringRequest.REQUEST_TYPE_PLACEHOLDER;
+ request.getParcel().connectivityScope = CONNECTIVITY_SCOPE_GLOBAL;
+ return request;
+ }
}
diff --git a/Tethering/tests/unit/Android.bp b/Tethering/tests/unit/Android.bp
index d0d23ac..ee82776 100644
--- a/Tethering/tests/unit/Android.bp
+++ b/Tethering/tests/unit/Android.bp
@@ -57,6 +57,7 @@
"mockito-target-extended-minus-junit4",
"net-tests-utils",
"testables",
+ "truth",
],
// TODO(b/147200698) change sdk_version to module-current and
// remove framework-minus-apex, ext, and framework-res
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index 680e81d..84b301f 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -48,7 +48,6 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.clearInvocations;
@@ -74,6 +73,7 @@
import android.net.LinkProperties;
import android.net.MacAddress;
import android.net.RouteInfo;
+import android.net.TetheringManager.TetheringRequest;
import android.net.dhcp.DhcpServerCallbacks;
import android.net.dhcp.DhcpServingParamsParcel;
import android.net.dhcp.IDhcpEventCallbacks;
@@ -240,7 +240,8 @@
Set<LinkAddress> upstreamAddresses, boolean usingLegacyDhcp, boolean usingBpfOffload)
throws Exception {
initStateMachine(interfaceType, usingLegacyDhcp, usingBpfOffload);
- dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+ createMockTetheringRequest(CONNECTIVITY_SCOPE_GLOBAL));
verify(mBpfCoordinator).addIpServer(mIpServer);
if (upstreamIface != null) {
InterfaceParams interfaceParams = mDependencies.getInterfaceParams(upstreamIface);
@@ -345,7 +346,8 @@
public void canBeTetheredAsBluetooth() throws Exception {
initStateMachine(TETHERING_BLUETOOTH);
- dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+ createMockTetheringRequest(CONNECTIVITY_SCOPE_GLOBAL));
InOrder inOrder = inOrder(mCallback, mNetd, mRoutingCoordinatorManager);
if (isAtLeastT()) {
inOrder.verify(mRoutingCoordinatorManager)
@@ -400,7 +402,8 @@
public void canBeTetheredAsUsb() throws Exception {
initStateMachine(TETHERING_USB);
- dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+ createMockTetheringRequest(CONNECTIVITY_SCOPE_GLOBAL));
InOrder inOrder = inOrder(mCallback, mNetd, mRoutingCoordinatorManager);
inOrder.verify(mRoutingCoordinatorManager).requestStickyDownstreamAddress(anyInt(),
eq(CONNECTIVITY_SCOPE_GLOBAL), any());
@@ -423,7 +426,8 @@
public void canBeTetheredAsWifiP2p_NotUsingDedicatedIp() throws Exception {
initStateMachine(TETHERING_WIFI_P2P);
- dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_LOCAL_ONLY);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+ createMockTetheringRequest(CONNECTIVITY_SCOPE_LOCAL));
InOrder inOrder = inOrder(mCallback, mNetd, mRoutingCoordinatorManager);
inOrder.verify(mRoutingCoordinatorManager).requestStickyDownstreamAddress(anyInt(),
eq(CONNECTIVITY_SCOPE_LOCAL), any());
@@ -447,7 +451,8 @@
initStateMachine(TETHERING_WIFI_P2P, false /* usingLegacyDhcp */, DEFAULT_USING_BPF_OFFLOAD,
true /* shouldEnableWifiP2pDedicatedIp */);
- dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_LOCAL_ONLY);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+ createMockTetheringRequest(CONNECTIVITY_SCOPE_LOCAL));
InOrder inOrder = inOrder(mCallback, mNetd, mRoutingCoordinatorManager);
// When using WiFi P2p dedicated IP, the IpServer just picks the IP address without
// requesting for it at RoutingCoordinatorManager.
@@ -627,7 +632,8 @@
initStateMachine(TETHERING_USB);
doThrow(RemoteException.class).when(mNetd).tetherInterfaceAdd(IFACE_NAME);
- dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+ createMockTetheringRequest(CONNECTIVITY_SCOPE_GLOBAL));
InOrder usbTeardownOrder = inOrder(mNetd, mCallback);
usbTeardownOrder.verify(mNetd).interfaceSetCfg(
argThat(cfg -> IFACE_NAME.equals(cfg.ifName)));
@@ -713,7 +719,8 @@
@Test
public void startsDhcpServerOnNcm() throws Exception {
initStateMachine(TETHERING_NCM);
- dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_LOCAL_ONLY);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+ createMockTetheringRequest(CONNECTIVITY_SCOPE_LOCAL));
dispatchTetherConnectionChanged(UPSTREAM_IFACE);
assertDhcpStarted(new IpPrefix("192.168.42.0/24"));
@@ -722,7 +729,8 @@
@Test
public void testOnNewPrefixRequest() throws Exception {
initStateMachine(TETHERING_NCM);
- dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_LOCAL_ONLY);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+ createMockTetheringRequest(CONNECTIVITY_SCOPE_LOCAL));
final IDhcpEventCallbacks eventCallbacks;
final ArgumentCaptor<IDhcpEventCallbacks> dhcpEventCbsCaptor =
@@ -911,7 +919,8 @@
doNothing().when(mDependencies).makeDhcpServer(any(), mDhcpParamsCaptor.capture(),
cbCaptor.capture());
initStateMachine(TETHERING_WIFI);
- dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+ createMockTetheringRequest(CONNECTIVITY_SCOPE_GLOBAL));
verify(mDhcpServer, never()).startWithCallbacks(any(), any());
// No stop dhcp server because dhcp server is not created yet.
@@ -957,14 +966,22 @@
assertDhcpServingParams(mDhcpParamsCaptor.getValue(), expectedPrefix);
}
+ private TetheringRequest createMockTetheringRequest(int connectivityScope) {
+ TetheringRequest request = mock(TetheringRequest.class);
+ when(request.getConnectivityScope()).thenReturn(connectivityScope);
+ return request;
+ }
+
/**
* Send a command to the state machine under test, and run the event loop to idle.
*
* @param command One of the IpServer.CMD_* constants.
- * @param arg1 An additional argument to pass.
+ * @param arg1 An additional argument to pass.
+ * @param arg2 An additional argument to pass.
+ * @param obj An additional object to pass.
*/
- private void dispatchCommand(int command, int arg1) {
- mIpServer.sendMessage(command, arg1);
+ private void dispatchCommand(int command, int arg1, int arg2, Object obj) {
+ mIpServer.sendMessage(command, arg1, arg2, obj);
mLooper.dispatchAll();
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
index 16ebbbb..58e1894 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
@@ -38,6 +38,7 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.networkstack.apishim.ConstantsShim.KEY_CARRIER_SUPPORTS_TETHERING_BOOL;
+import static com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
import static com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
@@ -49,6 +50,7 @@
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
@@ -159,10 +161,20 @@
return super.getSystemServiceName(serviceClass);
}
+ @NonNull
@Override
public Context createContextAsUser(UserHandle user, int flags) {
+ if (mCreateContextAsUserException != null) {
+ throw mCreateContextAsUserException;
+ }
return mMockContext; // Return self for easier test injection.
}
+
+ private RuntimeException mCreateContextAsUserException = null;
+
+ private void setCreateContextAsUserException(RuntimeException e) {
+ mCreateContextAsUserException = e;
+ }
}
class TestDependencies extends EntitlementManager.Dependencies {
@@ -591,8 +603,24 @@
.onTetherProvisioningFailed(TETHERING_WIFI, FAILED_TETHERING_REASON);
}
+ @IgnoreUpTo(SC_V2)
@Test
- public void testUiProvisioningMultiUser() {
+ public void testUiProvisioningMultiUser_aboveT_createContextAsUserThrows() {
+ mMockContext.setCreateContextAsUserException(new IllegalStateException());
+ doTestUiProvisioningMultiUser(true, 1);
+ doTestUiProvisioningMultiUser(false, 1);
+ }
+
+ @IgnoreUpTo(SC_V2)
+ @Test
+ public void testUiProvisioningMultiUser_aboveT() {
+ doTestUiProvisioningMultiUser(true, 1);
+ doTestUiProvisioningMultiUser(false, 0);
+ }
+
+ @IgnoreAfter(SC_V2)
+ @Test
+ public void testUiProvisioningMultiUser_belowT() {
doTestUiProvisioningMultiUser(true, 1);
doTestUiProvisioningMultiUser(false, 1);
}
@@ -630,6 +658,7 @@
doReturn(isAdminUser).when(mUserManager).isAdminUser();
mDeps.reset();
+ clearInvocations(mTetherProvisioningFailedListener);
mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
mEnMgr.notifyUpstream(true);
mLooper.dispatchAll();
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/FakeTetheringConfiguration.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/FakeTetheringConfiguration.java
index 087be26..c97fa3d 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/FakeTetheringConfiguration.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/FakeTetheringConfiguration.java
@@ -33,6 +33,11 @@
}
@Override
+ boolean isFeatureNotChickenedOut(@NonNull Context context, @NonNull String name) {
+ return true;
+ }
+
+ @Override
boolean getDeviceConfigBoolean(@NonNull String namespace, @NonNull String name,
boolean defaultValue) {
return defaultValue;
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
index f9e3a6a..ada88fb 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
@@ -26,7 +26,6 @@
import static android.net.TetheringManager.TETHERING_WIFI_P2P;
import static android.net.ip.IpServer.CMD_NOTIFY_PREFIX_CONFLICT;
-import static com.android.net.module.util.PrivateAddressCoordinator.TETHER_FORCE_RANDOM_PREFIX_BASE_SELECTION;
import static com.android.networkstack.tethering.util.PrefixUtils.asIpPrefix;
import static org.junit.Assert.assertEquals;
@@ -51,6 +50,7 @@
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.ip.IpServer;
+import android.os.Build;
import android.os.IBinder;
import androidx.test.filters.SmallTest;
@@ -58,8 +58,10 @@
import com.android.net.module.util.IIpv4PrefixRequest;
import com.android.net.module.util.PrivateAddressCoordinator;
+import com.android.testutils.DevSdkIgnoreRule;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -71,6 +73,9 @@
@RunWith(AndroidJUnit4.class)
@SmallTest
public final class PrivateAddressCoordinatorTest {
+ @Rule
+ public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
private static final String TEST_IFNAME = "test0";
@Mock private IpServer mHotspotIpServer;
@@ -231,11 +236,9 @@
assertEquals(usbAddress, newUsbAddress);
final UpstreamNetworkState wifiUpstream = buildUpstreamNetworkState(mWifiNetwork,
- new LinkAddress("192.168.88.23/16"), null,
- makeNetworkCapabilities(TRANSPORT_WIFI));
+ hotspotAddress, null, makeNetworkCapabilities(TRANSPORT_WIFI));
updateUpstreamPrefix(wifiUpstream);
verify(mHotspotIpServer).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
- verify(mUsbIpServer).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
}
private UpstreamNetworkState buildUpstreamNetworkState(final Network network,
@@ -323,10 +326,9 @@
assertFalse(localHotspotPrefix.containsPrefix(hotspotPrefix));
}
+ @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@Test
public void testStartedPrefixRange() throws Exception {
- when(mDeps.isFeatureEnabled(TETHER_FORCE_RANDOM_PREFIX_BASE_SELECTION)).thenReturn(true);
-
startedPrefixBaseTest("192.168.0.0/16", 0);
startedPrefixBaseTest("192.168.0.0/16", 1);
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/RequestTrackerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/RequestTrackerTest.java
new file mode 100644
index 0000000..e00e9f0
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/RequestTrackerTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.networkstack.tethering;
+
+import static android.net.TetheringManager.TETHERING_USB;
+import static android.net.TetheringManager.TETHERING_WIFI;
+
+import static com.android.networkstack.tethering.util.TetheringUtils.createPlaceholderRequest;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.net.TetheringManager.TetheringRequest;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.networkstack.tethering.RequestTracker.AddResult;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RequestTrackerTest {
+ private RequestTracker mRequestTracker;
+
+ @Before
+ public void setUp() {
+ mRequestTracker = new RequestTracker();
+ }
+
+ @Test
+ public void testNoRequestsAdded_noPendingRequests() {
+ assertThat(mRequestTracker.getNextPendingRequest(TETHERING_WIFI)).isNull();
+ assertThat(mRequestTracker.getOrCreatePendingRequest(TETHERING_WIFI))
+ .isEqualTo(createPlaceholderRequest(TETHERING_WIFI));
+ }
+
+ @Test
+ public void testAddRequest_successResultAndBecomesNextPending() {
+ final TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI).build();
+
+ final AddResult result = mRequestTracker.addPendingRequest(request);
+
+ assertThat(result).isEqualTo(AddResult.SUCCESS);
+ assertThat(mRequestTracker.getNextPendingRequest(TETHERING_WIFI)).isEqualTo(request);
+ assertThat(mRequestTracker.getOrCreatePendingRequest(TETHERING_WIFI)).isEqualTo(request);
+ }
+
+ @Test
+ public void testAddRequest_equalRequestExists_successResultAndBecomesNextPending() {
+ final TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI).build();
+ mRequestTracker.addPendingRequest(request);
+
+ final TetheringRequest equalRequest = new TetheringRequest.Builder(TETHERING_WIFI).build();
+ final AddResult result = mRequestTracker.addPendingRequest(equalRequest);
+
+ assertThat(result).isEqualTo(AddResult.SUCCESS);
+ assertThat(mRequestTracker.getNextPendingRequest(TETHERING_WIFI)).isEqualTo(request);
+ assertThat(mRequestTracker.getOrCreatePendingRequest(TETHERING_WIFI)).isEqualTo(request);
+ }
+
+ @Test
+ public void testAddRequest_equalButDifferentUidRequest_successResultAndBecomesNextPending() {
+ final TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI).build();
+ request.setUid(1000);
+ request.setPackageName("package");
+ final TetheringRequest differentUid = new TetheringRequest.Builder(TETHERING_WIFI).build();
+ differentUid.setUid(2000);
+ differentUid.setPackageName("package2");
+ mRequestTracker.addPendingRequest(request);
+
+ final AddResult result = mRequestTracker.addPendingRequest(differentUid);
+
+ assertThat(result).isEqualTo(AddResult.SUCCESS);
+ assertThat(mRequestTracker.getNextPendingRequest(TETHERING_WIFI)).isEqualTo(differentUid);
+ assertThat(mRequestTracker.getOrCreatePendingRequest(TETHERING_WIFI))
+ .isEqualTo(differentUid);
+ }
+
+ @Test
+ public void testAddConflictingRequest_returnsFailureConflictingPendingRequest() {
+ final TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI).build();
+ final TetheringRequest conflictingRequest = new TetheringRequest.Builder(TETHERING_WIFI)
+ .setExemptFromEntitlementCheck(true).build();
+ mRequestTracker.addPendingRequest(request);
+
+ final AddResult result = mRequestTracker.addPendingRequest(conflictingRequest);
+
+ assertThat(result).isEqualTo(AddResult.FAILURE_CONFLICTING_PENDING_REQUEST);
+ assertThat(mRequestTracker.getNextPendingRequest(TETHERING_WIFI)).isEqualTo(request);
+ assertThat(mRequestTracker.getOrCreatePendingRequest(TETHERING_WIFI)).isEqualTo(request);
+ }
+
+ @Test
+ public void testRemoveAllPendingRequests_noPendingRequestsLeft() {
+ final TetheringRequest firstRequest = new TetheringRequest.Builder(TETHERING_WIFI).build();
+ firstRequest.setUid(1000);
+ firstRequest.setPackageName("package");
+ mRequestTracker.addPendingRequest(firstRequest);
+ final TetheringRequest secondRequest = new TetheringRequest.Builder(TETHERING_WIFI).build();
+ secondRequest.setUid(2000);
+ secondRequest.setPackageName("package2");
+ mRequestTracker.addPendingRequest(secondRequest);
+
+ mRequestTracker.removeAllPendingRequests(TETHERING_WIFI);
+
+ assertThat(mRequestTracker.getNextPendingRequest(TETHERING_WIFI)).isNull();
+ assertThat(mRequestTracker.getOrCreatePendingRequest(TETHERING_WIFI))
+ .isEqualTo(createPlaceholderRequest(TETHERING_WIFI));
+ }
+
+ @Test
+ public void testRemoveAllPendingRequests_differentTypeExists_doesNotRemoveDifferentType() {
+ final TetheringRequest differentType = new TetheringRequest.Builder(TETHERING_USB).build();
+ mRequestTracker.addPendingRequest(differentType);
+
+ mRequestTracker.removeAllPendingRequests(TETHERING_WIFI);
+
+ assertThat(mRequestTracker.getNextPendingRequest(TETHERING_USB)).isEqualTo(differentType);
+ assertThat(mRequestTracker.getOrCreatePendingRequest(TETHERING_USB))
+ .isEqualTo(differentType);
+ }
+}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
index dd51c7a..0159573 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
@@ -160,6 +160,11 @@
}
@Override
+ boolean isFeatureNotChickenedOut(@NonNull Context context, @NonNull String name) {
+ return isMockFlagEnabled(name, true /* defaultEnabled */);
+ }
+
+ @Override
boolean getDeviceConfigBoolean(@NonNull String namespace, @NonNull String name,
boolean defaultValue) {
// Flags should use isFeatureEnabled instead of getBoolean; see comments in
@@ -767,9 +772,9 @@
@Test
public void testEnableSyncSMFlag() throws Exception {
- // Test default disabled
+ // Test default enabled
setTetherEnableSyncSMFlagEnabled(null);
- assertEnableSyncSM(false);
+ assertEnableSyncSM(true);
setTetherEnableSyncSMFlagEnabled(true);
assertEnableSyncSM(true);
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
index cc80251..b550ada 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
@@ -53,7 +53,6 @@
import android.net.TetheringManager;
import android.net.TetheringManager.TetheringRequest;
import android.net.TetheringRequestParcel;
-import android.net.ip.IpServer;
import android.os.Bundle;
import android.os.ConditionVariable;
import android.os.Handler;
@@ -219,7 +218,7 @@
mTetheringConnector.tether(TEST_IFACE_NAME, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG, result);
verify(mTethering).isTetheringSupported();
verify(mTethering).isTetheringAllowed();
- verify(mTethering).tether(TEST_IFACE_NAME, IpServer.STATE_TETHERED, result);
+ verify(mTethering).legacyTether(TEST_IFACE_NAME, result);
}
@Test
@@ -267,7 +266,7 @@
result);
verify(mTethering).isTetheringSupported();
verify(mTethering).isTetheringAllowed();
- verify(mTethering).untether(eq(TEST_IFACE_NAME), eq(result));
+ verify(mTethering).legacyUntether(eq(TEST_IFACE_NAME), eq(result));
}
@Test
@@ -661,7 +660,7 @@
mTetheringConnector.stopAllTethering(TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG, result);
verify(mTethering).isTetheringSupported();
verify(mTethering).isTetheringAllowed();
- verify(mTethering).untetherAll();
+ verify(mTethering).stopAllTethering();
result.assertResult(TETHER_ERROR_NO_ERROR);
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
index 97758cf..51efaf8 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -93,7 +93,6 @@
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.notNull;
@@ -933,11 +932,18 @@
// it creates a IpServer and sends out a broadcast indicating that the
// interface is "available".
if (emulateInterfaceStatusChanged) {
- // There is 1 IpServer state change event: STATE_AVAILABLE
- verify(mNotificationUpdater, times(1)).onDownstreamChanged(DOWNSTREAM_NONE);
- verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
- verify(mWifiManager).updateInterfaceIpState(
- TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ if (!SdkLevel.isAtLeastB()) {
+ // There is 1 IpServer state change event: STATE_AVAILABLE
+ verify(mNotificationUpdater, times(1)).onDownstreamChanged(DOWNSTREAM_NONE);
+ verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
+ verify(mWifiManager).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ } else {
+ // Starting in B, ignore the interfaceStatusChanged
+ verify(mNotificationUpdater, never()).onDownstreamChanged(DOWNSTREAM_NONE);
+ verify(mWifiManager, never()).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ }
}
verifyNoMoreInteractions(mNetd);
verifyNoMoreInteractions(mWifiManager);
@@ -957,8 +963,8 @@
mTethering.startTethering(request, TEST_CALLER_PKG, null);
mLooper.dispatchAll();
- assertEquals(1, mTethering.getActiveTetheringRequests().size());
- assertEquals(request, mTethering.getActiveTetheringRequests().get(TETHERING_USB));
+ assertEquals(1, mTethering.getPendingTetheringRequests().size());
+ assertTrue(mTethering.getPendingTetheringRequests().get(0).equals(request));
if (mTethering.getTetheringConfiguration().isUsingNcm()) {
verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_NCM);
@@ -1934,7 +1940,6 @@
workingLocalOnlyHotspotEnrichedApBroadcast(false);
}
- // TODO: Test with and without interfaceStatusChanged().
@Test
public void failingWifiTetheringLegacyApBroadcast() throws Exception {
initTetheringOnTestThread();
@@ -1953,12 +1958,20 @@
// tethering mode is to be started.
mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
sendWifiApStateChanged(WIFI_AP_STATE_ENABLED);
+ mLooper.dispatchAll();
- // There is 1 IpServer state change event: STATE_AVAILABLE
- verify(mNotificationUpdater, times(1)).onDownstreamChanged(DOWNSTREAM_NONE);
- verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
- verify(mWifiManager).updateInterfaceIpState(
- TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ if (!SdkLevel.isAtLeastB()) {
+ // There is 1 IpServer state change event: STATE_AVAILABLE from interfaceStatusChanged
+ verify(mNotificationUpdater, times(1)).onDownstreamChanged(DOWNSTREAM_NONE);
+ verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
+ verify(mWifiManager).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ } else {
+ // Starting in B, ignore the interfaceStatusChanged
+ verify(mNotificationUpdater, never()).onDownstreamChanged(DOWNSTREAM_NONE);
+ verify(mWifiManager, never()).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ }
verifyNoMoreInteractions(mNetd);
verifyNoMoreInteractions(mWifiManager);
}
@@ -2098,7 +2111,7 @@
verify(mNotificationUpdater, times(expectedInteractionsWithShowNotification))
.notifyTetheringDisabledByRestriction();
- verify(mockTethering, times(expectedInteractionsWithShowNotification)).untetherAll();
+ verify(mockTethering, times(expectedInteractionsWithShowNotification)).stopAllTethering();
}
@Test
@@ -2170,7 +2183,7 @@
runUsbTethering(upstreamState);
assertContains(Arrays.asList(mTethering.getTetheredIfaces()), TEST_RNDIS_IFNAME);
assertTrue(mTethering.isTetheringActive());
- assertEquals(0, mTethering.getActiveTetheringRequests().size());
+ assertEquals(0, mTethering.getPendingTetheringRequests().size());
final Tethering.UserRestrictionActionListener ural = makeUserRestrictionActionListener(
mTethering, false /* currentDisallow */, true /* nextDisallow */);
@@ -2361,14 +2374,19 @@
UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
initTetheringUpstream(upstreamState);
when(mWifiManager.startTetheredHotspot(null)).thenReturn(true);
- mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
- mLooper.dispatchAll();
- tetherState = callback.pollTetherStatesChanged();
- assertArrayEquals(tetherState.availableList, new TetheringInterface[] {wifiIface});
mTethering.startTethering(createTetheringRequest(TETHERING_WIFI), TEST_CALLER_PKG,
null);
+ mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
+ mLooper.dispatchAll();
+ if (SdkLevel.isAtLeastB()) {
+ // Starting in B, ignore the interfaceStatusChanged
+ callback.assertNoStateChangeCallback();
+ }
sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+ mLooper.dispatchAll();
+ tetherState = callback.pollTetherStatesChanged();
+ assertArrayEquals(tetherState.availableList, new TetheringInterface[] {wifiIface});
tetherState = callback.pollTetherStatesChanged();
assertArrayEquals(tetherState.tetheredList, new TetheringInterface[] {wifiIface});
callback.expectUpstreamChanged(upstreamState.network);
@@ -2459,19 +2477,25 @@
UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
initTetheringUpstream(upstreamState);
when(mWifiManager.startTetheredHotspot(null)).thenReturn(true);
+
+ // Enable wifi tethering
+ mBinderCallingUid = TEST_CALLER_UID;
+ mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, null);
mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
mLooper.dispatchAll();
+ if (SdkLevel.isAtLeastB()) {
+ // Starting in B, ignore the interfaceStatusChanged
+ callback.assertNoStateChangeCallback();
+ }
+ sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+ mLooper.dispatchAll();
+ // Verify we see Available -> Tethered states
assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig},
callback.pollTetherStatesChanged().availableList);
assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig},
differentCallback.pollTetherStatesChanged().availableList);
assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig},
settingsCallback.pollTetherStatesChanged().availableList);
-
- // Enable wifi tethering
- mBinderCallingUid = TEST_CALLER_UID;
- mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, null);
- sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
assertArrayEquals(new TetheringInterface[] {wifiIfaceWithConfig},
callback.pollTetherStatesChanged().tetheredList);
assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig},
@@ -2762,6 +2786,10 @@
public void assertHasResult() {
if (!mHasResult) fail("No callback result");
}
+
+ public void assertDoesNotHaveResult() {
+ if (mHasResult) fail("Has callback result");
+ }
}
@Test
@@ -2851,6 +2879,44 @@
}
@Test
+ @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testRequestStaticIpLegacyTether() throws Exception {
+ initTetheringOnTestThread();
+
+ // Call startTethering with static ip
+ final LinkAddress serverLinkAddr = new LinkAddress("192.168.0.123/24");
+ final LinkAddress clientLinkAddr = new LinkAddress("192.168.0.42/24");
+ final String serverAddr = "192.168.0.123";
+ final int clientAddrParceled = 0xc0a8002a;
+ final ArgumentCaptor<DhcpServingParamsParcel> dhcpParamsCaptor =
+ ArgumentCaptor.forClass(DhcpServingParamsParcel.class);
+ when(mWifiManager.startTetheredHotspot(any())).thenReturn(true);
+ mTethering.startTethering(createTetheringRequest(TETHERING_WIFI,
+ serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL, null),
+ TEST_CALLER_PKG, null);
+ mLooper.dispatchAll();
+ verify(mWifiManager, times(1)).startTetheredHotspot(any());
+ mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
+
+ // Call legacyTether on the interface before the link layer event comes back.
+ // This happens, for example, in pre-T bluetooth tethering: Settings calls startTethering,
+ // and then the bluetooth code calls the tether() API.
+ final ResultListener tetherResult = new ResultListener(TETHER_ERROR_NO_ERROR);
+ mTethering.legacyTether(TEST_WLAN_IFNAME, tetherResult);
+ mLooper.dispatchAll();
+ tetherResult.assertHasResult();
+
+ // Verify that the static ip set in startTethering is used
+ verify(mNetd).interfaceSetCfg(argThat(cfg -> serverAddr.equals(cfg.ipv4Addr)));
+ verify(mIpServerDependencies, times(1)).makeDhcpServer(any(), dhcpParamsCaptor.capture(),
+ any());
+ final DhcpServingParamsParcel params = dhcpParamsCaptor.getValue();
+ assertEquals(serverAddr, intToInet4AddressHTH(params.serverAddr).getHostAddress());
+ assertEquals(24, params.serverAddrPrefixLength);
+ assertEquals(clientAddrParceled, params.singleClientAddr);
+ }
+
+ @Test
public void testUpstreamNetworkChanged() throws Exception {
initTetheringOnTestThread();
final InOrder inOrder = inOrder(mNotificationUpdater);
@@ -3370,10 +3436,9 @@
}
@Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
public void testBluetoothTethering() throws Exception {
initTetheringOnTestThread();
- // Switch to @IgnoreUpTo(Build.VERSION_CODES.S_V2) when it is available for AOSP.
- assumeTrue(isAtLeastT());
final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
@@ -3407,10 +3472,9 @@
}
@Test
+ @IgnoreAfter(Build.VERSION_CODES.S_V2)
public void testBluetoothTetheringBeforeT() throws Exception {
initTetheringOnTestThread();
- // Switch to @IgnoreAfter(Build.VERSION_CODES.S_V2) when it is available for AOSP.
- assumeFalse(isAtLeastT());
final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
@@ -3426,7 +3490,7 @@
mTethering.interfaceStatusChanged(TEST_BT_IFNAME, false);
mTethering.interfaceStatusChanged(TEST_BT_IFNAME, true);
final ResultListener tetherResult = new ResultListener(TETHER_ERROR_NO_ERROR);
- mTethering.tether(TEST_BT_IFNAME, IpServer.STATE_TETHERED, tetherResult);
+ mTethering.legacyTether(TEST_BT_IFNAME, tetherResult);
mLooper.dispatchAll();
tetherResult.assertHasResult();
@@ -3446,7 +3510,7 @@
mTethering.stopTethering(TETHERING_BLUETOOTH);
mLooper.dispatchAll();
final ResultListener untetherResult = new ResultListener(TETHER_ERROR_NO_ERROR);
- mTethering.untether(TEST_BT_IFNAME, untetherResult);
+ mTethering.legacyUntether(TEST_BT_IFNAME, untetherResult);
mLooper.dispatchAll();
untetherResult.assertHasResult();
verifySetBluetoothTethering(false /* enable */, false /* bindToPanService */);
@@ -3476,7 +3540,7 @@
mTethering.interfaceStatusChanged(TEST_BT_IFNAME, false);
mTethering.interfaceStatusChanged(TEST_BT_IFNAME, true);
final ResultListener tetherResult = new ResultListener(TETHER_ERROR_NO_ERROR);
- mTethering.tether(TEST_BT_IFNAME, IpServer.STATE_TETHERED, tetherResult);
+ mTethering.legacyTether(TEST_BT_IFNAME, tetherResult);
mLooper.dispatchAll();
tetherResult.assertHasResult();
}
@@ -3490,6 +3554,23 @@
verifyNetdCommandForBtTearDown();
}
+ @Test
+ public void testPendingPanEnableRequestFailedUponDisableRequest() throws Exception {
+ initTetheringOnTestThread();
+
+ mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
+ final ResultListener failedEnable = new ResultListener(TETHER_ERROR_SERVICE_UNAVAIL);
+ mTethering.startTethering(createTetheringRequest(TETHERING_BLUETOOTH),
+ TEST_CALLER_PKG, failedEnable);
+ mLooper.dispatchAll();
+ failedEnable.assertDoesNotHaveResult();
+
+ // Stop tethering before the pan service connects. This should fail the enable request.
+ mTethering.stopTethering(TETHERING_BLUETOOTH);
+ mLooper.dispatchAll();
+ failedEnable.assertHasResult();
+ }
+
private void mockBluetoothSettings(boolean bluetoothOn, boolean tetheringOn) {
when(mBluetoothAdapter.isEnabled()).thenReturn(bluetoothOn);
when(mBluetoothPan.isTetheringOn()).thenReturn(tetheringOn);
diff --git a/bpf/dns_helper/DnsBpfHelper.cpp b/bpf/dns_helper/DnsBpfHelper.cpp
index 0719ade..cf2fa2b 100644
--- a/bpf/dns_helper/DnsBpfHelper.cpp
+++ b/bpf/dns_helper/DnsBpfHelper.cpp
@@ -32,12 +32,44 @@
} \
} while (0)
+// copied from BpfHandler.cpp
+static bool mainlineNetBpfLoadDone() {
+ return !access("/sys/fs/bpf/netd_shared/mainline_done", F_OK);
+}
+
+// copied from BpfHandler.cpp
+static inline void waitForNetProgsLoaded() {
+ // infinite loop until success with 5/10/20/40/60/60/60... delay
+ for (int delay = 5;; delay *= 2) {
+ if (delay > 60) delay = 60;
+ if (base::WaitForProperty("init.svc.mdnsd_netbpfload", "stopped", std::chrono::seconds(delay))
+ && mainlineNetBpfLoadDone()) return;
+ LOG(WARNING) << "Waited " << delay << "s for init.svc.mdnsd_netbpfload=stopped, still waiting.";
+ }
+}
+
base::Result<void> DnsBpfHelper::init() {
- if (!android::modules::sdklevel::IsAtLeastT()) {
- LOG(ERROR) << __func__ << ": Unsupported before Android T.";
+ if (!android::modules::sdklevel::IsAtLeastS()) {
+ LOG(ERROR) << __func__ << ": Unsupported before Android S.";
return base::Error(EOPNOTSUPP);
}
+ if (!android::modules::sdklevel::IsAtLeastT()) {
+ LOG(INFO) << "performing Android S mainline NetBpfload magic!";
+ if (!mainlineNetBpfLoadDone()) {
+ // We're on S/Sv2 & it's the first time netd is starting up (unless crashlooping)
+ if (!base::SetProperty("ctl.start", "mdnsd_netbpfload")) {
+ LOG(ERROR) << "Failed to set property ctl.start=mdnsd_netbpfload, see dmesg for reason.";
+ return base::Error(ENOEXEC);
+ }
+
+ LOG(INFO) << "Waiting for Networking BPF programs";
+ waitForNetProgsLoaded();
+ LOG(INFO) << "Networking BPF programs are loaded";
+ }
+ return {};
+ }
+
RETURN_IF_RESULT_NOT_OK(mConfigurationMap.init(CONFIGURATION_MAP_PATH));
RETURN_IF_RESULT_NOT_OK(mUidOwnerMap.init(UID_OWNER_MAP_PATH));
RETURN_IF_RESULT_NOT_OK(mDataSaverEnabledMap.init(DATA_SAVER_ENABLED_MAP_PATH));
diff --git a/bpf/headers/include/bpf/BpfClassic.h b/bpf/headers/include/bpf/BpfClassic.h
index e6cef89..26d8ad5 100644
--- a/bpf/headers/include/bpf/BpfClassic.h
+++ b/bpf/headers/include/bpf/BpfClassic.h
@@ -170,6 +170,9 @@
// IPv6 extension headers (HOPOPTS, DSTOPS, FRAG) begin with a u8 nexthdr
#define BPF_LOAD_NETX_RELATIVE_V6EXTHDR_NEXTHDR BPF_LOAD_NETX_RELATIVE_L4_U8(0)
+// IPv6 MLD start with u8 type
+#define BPF_LOAD_NETX_RELATIVE_MLD_TYPE BPF_LOAD_NETX_RELATIVE_L4_U8(0)
+
// IPv6 fragment header is always exactly 8 bytes long
#define BPF_LOAD_CONSTANT_V6FRAGHDR_LEN \
BPF_STMT(BPF_LD | BPF_IMM, 8)
diff --git a/bpf/headers/include/bpf/BpfMap.h b/bpf/headers/include/bpf/BpfMap.h
index 1037beb..576cca6 100644
--- a/bpf/headers/include/bpf/BpfMap.h
+++ b/bpf/headers/include/bpf/BpfMap.h
@@ -26,6 +26,7 @@
#include "BpfSyscallWrappers.h"
#include "bpf/BpfUtils.h"
+#include <cstdio>
#include <functional>
namespace android {
@@ -35,6 +36,30 @@
using base::unique_fd;
using std::function;
+#ifdef BPF_MAP_MAKE_VISIBLE_FOR_TESTING
+#undef BPFMAP_VERBOSE_ABORT
+#define BPFMAP_VERBOSE_ABORT
+#endif
+
+[[noreturn]] __attribute__((__format__(__printf__, 2, 3))) static inline
+void Abort(int __unused error, const char* __unused fmt, ...) {
+#ifdef BPFMAP_VERBOSE_ABORT
+ va_list va;
+ va_start(va, fmt);
+
+ fflush(stdout);
+ vfprintf(stderr, fmt, va);
+ if (error) fprintf(stderr, "; errno=%d [%s]", error, strerror(error));
+ putc('\n', stderr);
+ fflush(stderr);
+
+ va_end(va);
+#endif
+
+ abort();
+}
+
+
// This is a class wrapper for eBPF maps. The eBPF map is a special in-kernel
// data structure that stores data in <Key, Value> pairs. It can be read/write
// from userspace by passing syscalls with the map file descriptor. This class
@@ -60,14 +85,21 @@
protected:
void abortOnMismatch(bool writable) const {
- if (!mMapFd.ok()) abort();
+ if (!mMapFd.ok()) Abort(errno, "mMapFd %d is not valid", mMapFd.get());
if (isAtLeastKernelVersion(4, 14, 0)) {
int flags = bpfGetFdMapFlags(mMapFd);
- if (flags < 0) abort();
- if (flags & BPF_F_WRONLY) abort();
- if (writable && (flags & BPF_F_RDONLY)) abort();
- if (bpfGetFdKeySize(mMapFd) != sizeof(Key)) abort();
- if (bpfGetFdValueSize(mMapFd) != sizeof(Value)) abort();
+ if (flags < 0) Abort(errno, "bpfGetFdMapFlags fail: flags=%d", flags);
+ if (flags & BPF_F_WRONLY) Abort(0, "map is write-only (flags=0x%X)", flags);
+ if (writable && (flags & BPF_F_RDONLY))
+ Abort(0, "writable map is actually read-only (flags=0x%X)", flags);
+ int keySize = bpfGetFdKeySize(mMapFd);
+ if (keySize != sizeof(Key))
+ Abort(errno, "map key size mismatch (expected=%zu, actual=%d)",
+ sizeof(Key), keySize);
+ int valueSize = bpfGetFdValueSize(mMapFd);
+ if (valueSize != sizeof(Value))
+ Abort(errno, "map value size mismatch (expected=%zu, actual=%d)",
+ sizeof(Value), valueSize);
}
}
@@ -278,8 +310,8 @@
[[clang::reinitializes]] Result<void> resetMap(bpf_map_type map_type,
uint32_t max_entries,
uint32_t map_flags = 0) {
- if (map_flags & BPF_F_WRONLY) abort();
- if (map_flags & BPF_F_RDONLY) abort();
+ if (map_flags & BPF_F_WRONLY) Abort(0, "map_flags is write-only");
+ if (map_flags & BPF_F_RDONLY) Abort(0, "map_flags is read-only");
mMapFd.reset(createMap(map_type, sizeof(Key), sizeof(Value), max_entries,
map_flags));
if (!mMapFd.ok()) return ErrnoErrorf("BpfMap::resetMap() failed");
diff --git a/bpf/headers/include/bpf/BpfUtils.h b/bpf/headers/include/bpf/BpfUtils.h
index 9dd5822..ed08e1a 100644
--- a/bpf/headers/include/bpf/BpfUtils.h
+++ b/bpf/headers/include/bpf/BpfUtils.h
@@ -26,6 +26,7 @@
#include <sys/socket.h>
#include <sys/utsname.h>
+#include <android-base/properties.h>
#include <log/log.h>
#include "KernelUtils.h"
@@ -33,6 +34,16 @@
namespace android {
namespace bpf {
+const bool unreleased = (base::GetProperty("ro.build.version.codename", "REL") != "REL");
+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);
+
// See kernel's net/core/sock_diag.c __sock_gen_cookie()
// the implementation of which guarantees 0 will never be returned,
// primarily because 0 is used to mean not yet initialized,
@@ -63,9 +74,9 @@
// 4.9 kernels. The kernel code of socket release on pf_key socket will
// explicitly call synchronize_rcu() which is exactly what we need.
//
- // Linux 4.14/4.19/5.4/5.10/5.15/6.1 (and 6.3-rc5) still have this same behaviour.
+ // Linux 4.14/4.19/5.4/5.10/5.15/6.1/6.6/6.12 (& 6.13) have this behaviour.
// see net/key/af_key.c: pfkey_release() -> synchronize_rcu()
- // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/key/af_key.c?h=v6.3-rc5#n185
+ // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/key/af_key.c?h=v6.13#n185
const int pfSocket = socket(AF_KEY, SOCK_RAW | SOCK_CLOEXEC, PF_KEY_V2);
if (pfSocket < 0) {
diff --git a/bpf/headers/include/bpf/KernelUtils.h b/bpf/headers/include/bpf/KernelUtils.h
index 68bc607..a36085a 100644
--- a/bpf/headers/include/bpf/KernelUtils.h
+++ b/bpf/headers/include/bpf/KernelUtils.h
@@ -55,12 +55,12 @@
isKernelVersion(4, 9) || // minimum for Android S & T
isKernelVersion(4, 14) || // minimum for Android U
isKernelVersion(4, 19) || // minimum for Android V
- isKernelVersion(5, 4) || // first supported in Android R, min for W
+ isKernelVersion(5, 4) || // first supported in Android R, min for 25Q2
isKernelVersion(5, 10) || // first supported in Android S
isKernelVersion(5, 15) || // first supported in Android T
isKernelVersion(6, 1) || // first supported in Android U
isKernelVersion(6, 6) || // first supported in Android V
- isKernelVersion(6, 12); // first supported in Android W
+ isKernelVersion(6, 12); // first supported in Android 25Q2
}
// Figure out the bitness of userspace.
diff --git a/bpf/headers/include/bpf_helpers.h b/bpf/headers/include/bpf_helpers.h
index d817cc3..9d6b6f6 100644
--- a/bpf/headers/include/bpf_helpers.h
+++ b/bpf/headers/include/bpf_helpers.h
@@ -46,12 +46,12 @@
#define BPFLOADER_U_QPR2_VERSION 41u
#define BPFLOADER_PLATFORM_VERSION BPFLOADER_U_QPR2_VERSION
-// Android Mainline - this bpfloader should eventually go back to T (or even S)
+// Android Mainline BpfLoader when running on Android S (sdk=31)
// Note: this value (and the following +1u's) are hardcoded in NetBpfLoad.cpp
-#define BPFLOADER_MAINLINE_VERSION 42u
+#define BPFLOADER_MAINLINE_S_VERSION 42u
// Android Mainline BpfLoader when running on Android T (sdk=33)
-#define BPFLOADER_MAINLINE_T_VERSION (BPFLOADER_MAINLINE_VERSION + 1u)
+#define BPFLOADER_MAINLINE_T_VERSION (BPFLOADER_MAINLINE_S_VERSION + 1u)
// Android Mainline BpfLoader when running on Android U (sdk=34)
#define BPFLOADER_MAINLINE_U_VERSION (BPFLOADER_MAINLINE_T_VERSION + 1u)
@@ -112,7 +112,7 @@
unsigned int _bpfloader_max_ver SECTION("bpfloader_max_ver") = BPFLOADER_MAX_VER; \
size_t _size_of_bpf_map_def SECTION("size_of_bpf_map_def") = sizeof(struct bpf_map_def); \
size_t _size_of_bpf_prog_def SECTION("size_of_bpf_prog_def") = sizeof(struct bpf_prog_def); \
- unsigned _btf_min_bpfloader_ver SECTION("btf_min_bpfloader_ver") = BPFLOADER_MAINLINE_VERSION; \
+ unsigned _btf_min_bpfloader_ver SECTION("btf_min_bpfloader_ver") = BPFLOADER_MAINLINE_S_VERSION; \
unsigned _btf_user_min_bpfloader_ver SECTION("btf_user_min_bpfloader_ver") = 0xFFFFFFFFu; \
char _license[] SECTION("license") = (NAME)
@@ -122,28 +122,46 @@
*/
#define CRITICAL(REASON) char _critical[] SECTION("critical") = (REASON)
-/*
- * Helper functions called from eBPF programs written in C. These are
- * implemented in the kernel sources.
- */
+// Helpers for writing kernel version specific bpf programs
struct kver_uint { unsigned int kver; };
#define KVER_(v) ((struct kver_uint){ .kver = (v) })
#define KVER(a, b, c) KVER_(((a) << 24) + ((b) << 16) + (c))
#define KVER_NONE KVER_(0)
+#define KVER_4_9 KVER(4, 9, 0)
#define KVER_4_14 KVER(4, 14, 0)
#define KVER_4_19 KVER(4, 19, 0)
#define KVER_5_4 KVER(5, 4, 0)
-#define KVER_5_8 KVER(5, 8, 0)
-#define KVER_5_9 KVER(5, 9, 0)
#define KVER_5_10 KVER(5, 10, 0)
#define KVER_5_15 KVER(5, 15, 0)
#define KVER_6_1 KVER(6, 1, 0)
#define KVER_6_6 KVER(6, 6, 0)
+#define KVER_6_12 KVER(6, 12, 0)
#define KVER_INF KVER_(0xFFFFFFFFu)
#define KVER_IS_AT_LEAST(kver, a, b, c) ((kver).kver >= KVER(a, b, c).kver)
+// Helpers for writing sdk level specific bpf programs
+//
+// Note: we choose to follow sdk api level values, but there is no real need for this:
+// These just need to be monotonically increasing. We could also use values ten or even
+// a hundred times larger to leave room for quarters or months. We may also just use
+// dates or something (2502 or 202506 for 25Q2) or even the mainline bpfloader version...
+// For now this easily suffices for our use case.
+
+struct sdk_level_uint { unsigned int sdk_level; };
+#define SDK_LEVEL_(v) ((struct sdk_level_uint){ .sdk_level = (v) })
+#define SDK_LEVEL_NONE SDK_LEVEL_(0)
+#define SDK_LEVEL_S SDK_LEVEL_(31) // Android 12
+#define SDK_LEVEL_Sv2 SDK_LEVEL_(32) // Android 12L
+#define SDK_LEVEL_T SDK_LEVEL_(33) // Android 13
+#define SDK_LEVEL_U SDK_LEVEL_(34) // Android 14
+#define SDK_LEVEL_V SDK_LEVEL_(35) // Android 15
+#define SDK_LEVEL_24Q3 SDK_LEVEL_V
+#define SDK_LEVEL_25Q2 SDK_LEVEL_(36) // Android 16
+
+#define SDK_LEVEL_IS_AT_LEAST(lvl, v) ((lvl).sdk_level >= (SDK_LEVEL_##v).sdk_level)
+
/*
* BPFFS (ie. /sys/fs/bpf) labelling is as follows:
* subdirectory selinux context mainline usecase / usable by
@@ -168,6 +186,11 @@
* See cs/p:aosp-master%20-file:prebuilts/%20file:genfs_contexts%20"genfscon%20bpf"
*/
+/*
+ * Helper functions called from eBPF programs written in C. These are
+ * implemented in the kernel sources.
+ */
+
/* generic functions */
/*
@@ -268,14 +291,15 @@
// Type safe macro to declare a ring buffer and related output functions.
// Compatibility:
// * BPF ring buffers are only available kernels 5.8 and above. Any program
-// accessing the ring buffer should set a program level min_kver >= 5.8.
-// * The definition below sets a map min_kver of 5.8 which requires targeting
+// accessing the ring buffer should set a program level min_kver >= 5.10,
+// since 5.10 is the next LTS version.
+// * The definition below sets a map min_kver of 5.10 which requires targeting
// a BPFLOADER_MIN_VER >= BPFLOADER_S_VERSION.
#define DEFINE_BPF_RINGBUF_EXT(the_map, ValueType, size_bytes, usr, grp, md, \
selinux, pindir, share, min_loader, max_loader, \
ignore_eng, ignore_user, ignore_userdebug) \
DEFINE_BPF_MAP_BASE(the_map, RINGBUF, 0, 0, size_bytes, usr, grp, md, \
- selinux, pindir, share, KVER_5_8, KVER_INF, \
+ selinux, pindir, share, KVER_5_10, KVER_INF, \
min_loader, max_loader, ignore_eng, ignore_user, \
ignore_userdebug, 0); \
\
@@ -395,6 +419,22 @@
DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, \
DEFAULT_BPF_MAP_UID, gid, 0660)
+// idea from Linux include/linux/compiler_types.h (eBPF is always a 64-bit arch)
+#define NATIVE_WORD(t) ((sizeof(t) == 1) || (sizeof(t) == 2) || (sizeof(t) == 4) || (sizeof(t) == 8))
+
+// simplified from Linux include/asm-generic/rwonce.h
+#define READ_ONCE(x) \
+ ({ \
+ _Static_assert(NATIVE_WORD(x), "READ_ONCE requires a native word size"); \
+ (*(const volatile typeof(x) *)&(x)) \
+ })
+
+#define WRITE_ONCE(x, value) \
+ do { \
+ _Static_assert(NATIVE_WORD(x), "WRITE_ONCE requires a native word size"); \
+ *(volatile typeof(x) *)&(x) = (value); \
+ } while (0)
+
// LLVM eBPF builtins: they directly generate BPF_LD_ABS/BPF_LD_IND (skb may be ignored?)
unsigned long long load_byte(void* skb, unsigned long long off) asm("llvm.bpf.load.byte");
unsigned long long load_half(void* skb, unsigned long long off) asm("llvm.bpf.load.half");
diff --git a/bpf/headers/include/bpf_map_def.h b/bpf/headers/include/bpf_map_def.h
index d67da48..2e5afca 100644
--- a/bpf/headers/include/bpf_map_def.h
+++ b/bpf/headers/include/bpf_map_def.h
@@ -106,8 +106,12 @@
// Here sizeof & __alignof__ are consistent, but _Alignof is not: compile for 'aosp_cf_x86_phone'
_Static_assert(sizeof(unsigned long long) == 8, "sizeof unsigned long long != 8");
_Static_assert(__alignof__(unsigned long long) == 8, "__alignof__ unsigned long long != 8");
-// BPF wants 8, but 32-bit x86 wants 4
-//_Static_assert(_Alignof(unsigned long long) == 8, "_Alignof unsigned long long != 8");
+// BPF & everyone else wants 8, but 32-bit x86 wants 4
+#if defined(__i386__)
+_Static_assert(_Alignof(unsigned long long) == 4, "x86-32 _Alignof unsigned long long != 4");
+#else
+_Static_assert(_Alignof(unsigned long long) == 8, "_Alignof unsigned long long != 8");
+#endif
// for maps:
@@ -159,7 +163,7 @@
enum bpf_map_type type;
unsigned int key_size;
unsigned int value_size;
- int max_entries; // negative means BPF_F_NO_PREALLOC, but *might* not work with S
+ unsigned int max_entries;
unsigned int map_flags;
// The following are not supported by the Android bpfloader:
diff --git a/bpf/loader/Android.bp b/bpf/loader/Android.bp
index b08913a..345e92b 100644
--- a/bpf/loader/Android.bp
+++ b/bpf/loader/Android.bp
@@ -42,6 +42,7 @@
shared_libs: [
"libbase",
"liblog",
+ "libbpf",
],
srcs: ["NetBpfLoad.cpp"],
apex_available: [
@@ -56,11 +57,22 @@
installable: false,
}
-// Versioned netbpfload init rc: init system will process it only on api T/33+ devices
+// Versioned netbpfload init rc: init system will process it only on api R/30 S/31 Sv2/32 devices
// Note: R[30] S[31] Sv2[32] T[33] U[34] V[35])
//
// For details of versioned rc files see:
// https://android.googlesource.com/platform/system/core/+/HEAD/init/README.md#versioned-rc-files-within-apexs
+//
+// However, .Xrc versioning doesn't work on S, so we use unversioned, and thus *do* trigger on R,
+// luckily nothing ever uses the new service on R, so you can think of it as being S/Sv2 only
+prebuilt_etc {
+ name: "netbpfload.31rc",
+ src: "netbpfload.31rc",
+ filename: "netbpfload.rc", // intentional: .31rc wouldn't take effect on S
+ installable: false,
+}
+
+// Versioned netbpfload init rc: init system will process it only on api T/33+ devices
prebuilt_etc {
name: "netbpfload.33rc",
src: "netbpfload.33rc",
diff --git a/bpf/loader/NetBpfLoad.cpp b/bpf/loader/NetBpfLoad.cpp
index 53c7d49..b9ef766 100644
--- a/bpf/loader/NetBpfLoad.cpp
+++ b/bpf/loader/NetBpfLoad.cpp
@@ -17,6 +17,7 @@
#define LOG_TAG "NetBpfLoad"
#include <arpa/inet.h>
+#include <bpf/libbpf.h>
#include <dirent.h>
#include <elf.h>
#include <errno.h>
@@ -60,7 +61,8 @@
#include "bpf_map_def.h"
// The following matches bpf_helpers.h, which is only for inclusion in bpf code
-#define BPFLOADER_MAINLINE_VERSION 42u
+#define BPFLOADER_MAINLINE_S_VERSION 42u
+#define BPFLOADER_MAINLINE_25Q2_VERSION 47u
using android::base::EndsWith;
using android::base::GetIntProperty;
@@ -121,6 +123,7 @@
struct Location {
const char* const dir = "";
const char* const prefix = "";
+ const bool t_plus = true;
};
// Returns the build type string (from ro.build.type).
@@ -729,6 +732,12 @@
}
enum bpf_map_type type = md[i].type;
+ if (type == BPF_MAP_TYPE_LPM_TRIE && !isAtLeastKernelVersion(4, 14, 0)) {
+ // On Linux Kernels older than 4.14 this map type doesn't exist - autoskip.
+ ALOGD("skipping LPM_TRIE map %s - requires kver 4.14+", mapNames[i].c_str());
+ mapFds.push_back(unique_fd());
+ continue;
+ }
if (type == BPF_MAP_TYPE_DEVMAP && !isAtLeastKernelVersion(4, 14, 0)) {
// On Linux Kernels older than 4.14 this map type doesn't exist, but it can kind
// of be approximated: ARRAY has the same userspace api, though it is not usable
@@ -817,14 +826,14 @@
"tmp_map_" + objName + "_" + mapNames[i];
ret = bpfFdPin(fd, createLoc.c_str());
if (ret) {
- int err = errno;
+ const int err = errno;
ALOGE("create %s -> %d [%d:%s]", createLoc.c_str(), ret, err, strerror(err));
return -err;
}
ret = renameat2(AT_FDCWD, createLoc.c_str(),
AT_FDCWD, mapPinLoc.c_str(), RENAME_NOREPLACE);
if (ret) {
- int err = errno;
+ const int err = errno;
ALOGE("rename %s %s -> %d [%d:%s]", createLoc.c_str(), mapPinLoc.c_str(), ret,
err, strerror(err));
return -err;
@@ -832,32 +841,34 @@
} else {
ret = bpfFdPin(fd, mapPinLoc.c_str());
if (ret) {
- int err = errno;
+ const int err = errno;
ALOGE("pin %s -> %d [%d:%s]", mapPinLoc.c_str(), ret, err, strerror(err));
return -err;
}
}
ret = chmod(mapPinLoc.c_str(), md[i].mode);
if (ret) {
- int err = errno;
+ const int err = errno;
ALOGE("chmod(%s, 0%o) = %d [%d:%s]", mapPinLoc.c_str(), md[i].mode, ret, err,
strerror(err));
return -err;
}
ret = chown(mapPinLoc.c_str(), (uid_t)md[i].uid, (gid_t)md[i].gid);
if (ret) {
- int err = errno;
+ const int err = errno;
ALOGE("chown(%s, %u, %u) = %d [%d:%s]", mapPinLoc.c_str(), md[i].uid, md[i].gid,
ret, err, strerror(err));
return -err;
}
}
- int mapId = bpfGetFdMapId(fd);
- if (mapId == -1) {
- if (isAtLeastKernelVersion(4, 14, 0))
- ALOGE("bpfGetFdMapId failed, ret: %d [%d]", mapId, errno);
- } else {
+ if (isAtLeastKernelVersion(4, 14, 0)) {
+ int mapId = bpfGetFdMapId(fd);
+ if (mapId == -1) {
+ const int err = errno;
+ ALOGE("bpfGetFdMapId failed, errno: %d", err);
+ return -err;
+ }
ALOGI("map %s id %d", mapPinLoc.c_str(), mapId);
}
@@ -1000,7 +1011,7 @@
if (access(progPinLoc.c_str(), F_OK) == 0) {
fd.reset(retrieveProgram(progPinLoc.c_str()));
ALOGD("New bpf prog load reusing prog %s, ret: %d (%s)", progPinLoc.c_str(), fd.get(),
- (!fd.ok() ? std::strerror(errno) : "no error"));
+ !fd.ok() ? std::strerror(errno) : "ok");
reuse = true;
} else {
static char log_buf[1 << 20]; // 1 MiB logging buffer
@@ -1031,7 +1042,7 @@
ALOGD("BPF_PROG_LOAD call for %s (%s) returned '%s' fd: %d (%s)", elfPath,
cs[i].name.c_str(), log_oneline ? log_buf : "{multiline}",
- fd.get(), (!fd.ok() ? std::strerror(errno) : "ok"));
+ fd.get(), !fd.ok() ? std::strerror(errno) : "ok");
if (!fd.ok()) {
// kernel NULL terminates log_buf, so this checks for non-empty string
@@ -1060,14 +1071,14 @@
"tmp_prog_" + objName + '_' + string(name);
ret = bpfFdPin(fd, createLoc.c_str());
if (ret) {
- int err = errno;
+ const int err = errno;
ALOGE("create %s -> %d [%d:%s]", createLoc.c_str(), ret, err, strerror(err));
return -err;
}
ret = renameat2(AT_FDCWD, createLoc.c_str(),
AT_FDCWD, progPinLoc.c_str(), RENAME_NOREPLACE);
if (ret) {
- int err = errno;
+ const int err = errno;
ALOGE("rename %s %s -> %d [%d:%s]", createLoc.c_str(), progPinLoc.c_str(), ret,
err, strerror(err));
return -err;
@@ -1075,30 +1086,52 @@
} else {
ret = bpfFdPin(fd, progPinLoc.c_str());
if (ret) {
- int err = errno;
+ const int err = errno;
ALOGE("create %s -> %d [%d:%s]", progPinLoc.c_str(), ret, err, strerror(err));
return -err;
}
}
if (chmod(progPinLoc.c_str(), 0440)) {
- int err = errno;
+ const int err = errno;
ALOGE("chmod %s 0440 -> [%d:%s]", progPinLoc.c_str(), err, strerror(err));
return -err;
}
if (chown(progPinLoc.c_str(), (uid_t)cs[i].prog_def->uid,
(gid_t)cs[i].prog_def->gid)) {
- int err = errno;
+ const int err = errno;
ALOGE("chown %s %d %d -> [%d:%s]", progPinLoc.c_str(), cs[i].prog_def->uid,
cs[i].prog_def->gid, err, strerror(err));
return -err;
}
}
- int progId = bpfGetFdProgId(fd);
- if (progId == -1) {
- ALOGE("bpfGetFdProgId failed, ret: %d [%d]", progId, errno);
- } else {
- ALOGI("prog %s id %d", progPinLoc.c_str(), progId);
+ if (isAtLeastKernelVersion(4, 14, 0)) {
+ int progId = bpfGetFdProgId(fd);
+ if (progId == -1) {
+ const int err = errno;
+ ALOGE("bpfGetFdProgId failed, errno: %d", err);
+ return -err;
+ }
+
+ int jitLen = bpfGetFdJitProgLen(fd);
+ if (jitLen == -1) {
+ const int err = errno;
+ ALOGE("bpfGetFdJitProgLen failed, ret: %d", err);
+ return -err;
+ }
+
+ int xlatLen = bpfGetFdXlatProgLen(fd);
+ if (xlatLen == -1) {
+ const int err = errno;
+ ALOGE("bpfGetFdXlatProgLen failed, ret: %d", err);
+ return -err;
+ }
+ ALOGI("prog %s id %d len jit:%d xlat:%d", progPinLoc.c_str(), progId, jitLen, xlatLen);
+
+ if (!jitLen && bpfloader_ver >= BPFLOADER_MAINLINE_25Q2_VERSION) {
+ ALOGE("Kernel eBPF JIT failure for %s", progPinLoc.c_str());
+ return -ENOTSUP;
+ }
}
}
@@ -1156,7 +1189,7 @@
ret = readCodeSections(elfFile, cs);
// BPF .o's with no programs are only supported by mainline netbpfload,
// make sure .o's targeting non-mainline (ie. S) bpfloader don't show up.
- if (ret == -ENOENT && bpfLoaderMinVer >= BPFLOADER_MAINLINE_VERSION)
+ if (ret == -ENOENT && bpfLoaderMinVer >= BPFLOADER_MAINLINE_S_VERSION)
return 0;
if (ret) {
ALOGE("Couldn't read all code sections in %s", elfPath);
@@ -1185,8 +1218,9 @@
const Location locations[] = {
// S+ Tethering mainline module (network_stack): tether offload
{
- .dir = BPFROOT "/",
+ .dir = BPFROOT "/tethering/",
.prefix = "tethering/",
+ .t_plus = false,
},
// T+ Tethering mainline module (shared with netd & system server)
// netutils_wrapper (for iptables xt_bpf) has access to programs
@@ -1381,39 +1415,15 @@
}
static int doLoad(char** argv, char * const envp[]) {
+ if (!isAtLeastS) {
+ ALOGE("Impossible - not reachable on Android <S.");
+ // for safety, we don't fail, this is a just-in-case workaround
+ // for any possible busted 'optimized' start everything vendor init hacks on R
+ return 0;
+ }
+
const bool runningAsRoot = !getuid(); // true iff U QPR3 or V+
- // Any released device will have codename REL instead of a 'real' codename.
- // For safety: default to 'REL' so we default to unreleased=false on failure.
- const bool unreleased = (GetProperty("ro.build.version.codename", "REL") != "REL");
-
- // goog/main device_api_level is bumped *way* before aosp/main api level
- // (the latter only gets bumped during the push of goog/main to aosp/main)
- //
- // Since we develop in AOSP, we want it to behave as if it was bumped too.
- //
- // Note that AOSP doesn't really have a good api level (for example during
- // early V dev cycle, it would have *all* of T, some but not all of U, and some V).
- // One could argue that for our purposes AOSP api level should be infinite or 10000.
- //
- // This could also cause api to be increased in goog/main or other branches,
- // but I can't imagine a case where this would be a problem: the problem
- // is rather a too low api level, rather than some ill defined high value.
- // For example as I write this aosp is 34/U, and goog is 35/V,
- // we want to treat both goog & aosp as 35/V, but it's harmless if we
- // treat goog as 36 because that value isn't yet defined to mean anything,
- // and we thus never compare against it.
- //
- // Also note that 'android_get_device_api_level()' is what the
- // //system/core/init/apex_init_util.cpp
- // apex init .XXrc parsing code uses for XX filtering, and that code
- // (now) similarly uses __ANDROID_API_FUTURE__ for non 'REL' codenames.
- const int api_level = unreleased ? __ANDROID_API_FUTURE__ : android_get_device_api_level();
- const bool isAtLeastT = (api_level >= __ANDROID_API_T__);
- const bool isAtLeastU = (api_level >= __ANDROID_API_U__);
- const bool isAtLeastV = (api_level >= __ANDROID_API_V__);
- const bool isAtLeast25Q2 = (api_level > __ANDROID_API_V__); // TODO: fix >
-
const int first_api_level = GetIntProperty("ro.board.first_api_level", api_level);
// last in U QPR2 beta1
@@ -1422,17 +1432,19 @@
const bool has_platform_netbpfload_rc = exists("/system/etc/init/netbpfload.rc");
// Version of Network BpfLoader depends on the Android OS version
- unsigned int bpfloader_ver = BPFLOADER_MAINLINE_VERSION; // [42u]
+ unsigned int bpfloader_ver = BPFLOADER_MAINLINE_S_VERSION; // [42u]
if (isAtLeastT) ++bpfloader_ver; // [43] BPFLOADER_MAINLINE_T_VERSION
if (isAtLeastU) ++bpfloader_ver; // [44] BPFLOADER_MAINLINE_U_VERSION
if (runningAsRoot) ++bpfloader_ver; // [45] BPFLOADER_MAINLINE_U_QPR3_VERSION
if (isAtLeastV) ++bpfloader_ver; // [46] BPFLOADER_MAINLINE_V_VERSION
if (isAtLeast25Q2) ++bpfloader_ver; // [47] BPFLOADER_MAINLINE_25Q2_VERSION
- ALOGI("NetBpfLoad v0.%u (%s) api:%d/%d kver:%07x (%s) uid:%d rc:%d%d",
+ ALOGI("NetBpfLoad v0.%u (%s) api:%d/%d kver:%07x (%s) libbpf: v%u.%u "
+ "uid:%d rc:%d%d",
bpfloader_ver, argv[0], android_get_device_api_level(), api_level,
- kernelVersion(), describeArch(), getuid(),
- has_platform_bpfloader_rc, has_platform_netbpfload_rc);
+ kernelVersion(), describeArch(), libbpf_major_version(),
+ libbpf_minor_version(), getuid(), has_platform_bpfloader_rc,
+ has_platform_netbpfload_rc);
if (!has_platform_bpfloader_rc && !has_platform_netbpfload_rc) {
ALOGE("Unable to find platform's bpfloader & netbpfload init scripts.");
@@ -1446,14 +1458,9 @@
logTetheringApexVersion();
- if (!isAtLeastT) {
- ALOGE("Impossible - not reachable on Android <T.");
- return 1;
- }
-
// both S and T require kernel 4.9 (and eBpf support)
- if (isAtLeastT && !isAtLeastKernelVersion(4, 9, 0)) {
- ALOGE("Android T requires kernel 4.9.");
+ if (!isAtLeastKernelVersion(4, 9, 0)) {
+ ALOGE("Android S & T require kernel 4.9.");
return 1;
}
@@ -1484,6 +1491,11 @@
if (!isTV()) return 1;
}
+ if (isKernel32Bit() && isAtLeast25Q2) {
+ ALOGE("Android 25Q2 requires 64 bit kernel.");
+ return 1;
+ }
+
// 6.6 is highest version supported by Android V, so this is effectively W+ (sdk=36+)
if (isKernel32Bit() && isAtLeastKernelVersion(6, 7, 0)) {
ALOGE("Android platform with 32 bit kernel version >= 6.7.0 is unsupported");
@@ -1555,7 +1567,7 @@
if (isArm() && (isTV() || isWear())) {
// exempt Arm TV or Wear devices (arm32 ABI is far less problematic than x86-32)
ALOGW("[Arm TV/Wear] 32-bit userspace unsupported on 6.2+ kernels.");
- } else if (first_api_level <= __ANDROID_API_T__ && isArm()) {
+ } else if (first_api_level <= 33 /*T*/ && isArm()) {
// also exempt Arm devices upgrading with major kernel rev from T-
// might possibly be better for them to run with a newer kernel...
ALOGW("[Arm KernelUpRev] 32-bit userspace unsupported on 6.2+ kernels.");
@@ -1617,18 +1629,22 @@
// which could otherwise fail with ENOENT during object pinning or renaming,
// due to ordering issues)
for (const auto& location : locations) {
+ if (location.t_plus && !isAtLeastT) continue;
if (createSysFsBpfSubDir(location.prefix)) return 1;
}
- // Note: there's no actual src dir for fs_bpf_loader .o's,
- // so it is not listed in 'locations[].prefix'.
- // This is because this is primarily meant for triggering genfscon rules,
- // and as such this will likely always be the case.
- // Thus we need to manually create the /sys/fs/bpf/loader subdirectory.
- if (createSysFsBpfSubDir("loader")) return 1;
+ if (isAtLeastT) {
+ // Note: there's no actual src dir for fs_bpf_loader .o's,
+ // so it is not listed in 'locations[].prefix'.
+ // This is because this is primarily meant for triggering genfscon rules,
+ // and as such this will likely always be the case.
+ // Thus we need to manually create the /sys/fs/bpf/loader subdirectory.
+ if (createSysFsBpfSubDir("loader")) return 1;
+ }
// Load all ELF objects, create programs and maps, and pin them
for (const auto& location : locations) {
+ if (location.t_plus && !isAtLeastT) continue;
if (loadAllElfObjects(bpfloader_ver, location) != 0) {
ALOGE("=== CRITICAL FAILURE LOADING BPF PROGRAMS FROM %s ===", location.dir);
ALOGE("If this triggers reliably, you're probably missing kernel options or patches.");
@@ -1649,6 +1665,9 @@
return 1;
}
+ // on S we haven't created this subdir yet, but we need it for 'mainline_done' flag below
+ if (!isAtLeastT && createSysFsBpfSubDir("netd_shared")) return 1;
+
// leave a flag that we're done
if (createSysFsBpfSubDir("netd_shared/mainline_done")) return 1;
@@ -1683,7 +1702,12 @@
} // namespace android
int main(int argc, char** argv, char * const envp[]) {
- InitLogging(argv, &KernelLogger);
+ if (android::bpf::isAtLeastT) {
+ InitLogging(argv, &KernelLogger);
+ } else {
+ // S lacks the sepolicy to make non-root uid KernelLogger viable
+ InitLogging(argv);
+ }
if (argc == 2 && !strcmp(argv[1], "done")) {
// we're being re-exec'ed from platform bpfloader to 'finalize' things
diff --git a/bpf/loader/netbpfload.31rc b/bpf/loader/netbpfload.31rc
new file mode 100644
index 0000000..bca7dc8
--- /dev/null
+++ b/bpf/loader/netbpfload.31rc
@@ -0,0 +1,13 @@
+# This file takes effect only on S and Sv2
+# (Note: it does take effect on R as well, but isn't actually used)
+#
+# The service is started from netd's dnsresolver call into ADnsHelper_init()
+# on initial (boot time) startup of netd.
+
+service mdnsd_netbpfload /apex/com.android.tethering/bin/netbpfload
+ capabilities CHOWN SYS_ADMIN NET_ADMIN
+ group system root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw
+ user system
+ rlimit memlock 1073741824 1073741824
+ oneshot
+ reboot_on_failure reboot,netbpfload-failed
diff --git a/bpf/netd/BpfHandler.cpp b/bpf/netd/BpfHandler.cpp
index bcd0cba..d41aa81 100644
--- a/bpf/netd/BpfHandler.cpp
+++ b/bpf/netd/BpfHandler.cpp
@@ -22,7 +22,6 @@
#include <inttypes.h>
#include <android-base/unique_fd.h>
-#include <android-modules-utils/sdk_level.h>
#include <bpf/WaitForProgsLoaded.h>
#include <log/log.h>
#include <netdutils/UidConstants.h>
@@ -36,6 +35,12 @@
using base::unique_fd;
using base::WaitForProperty;
using bpf::getSocketCookie;
+using bpf::isAtLeastKernelVersion;
+using bpf::isAtLeastT;
+using bpf::isAtLeastU;
+using bpf::isAtLeastV;
+using bpf::isAtLeast25Q2;
+using bpf::queryProgram;
using bpf::retrieveProgram;
using netdutils::Status;
using netdutils::statusFromErrno;
@@ -56,7 +61,7 @@
if (!cgroupProg.ok()) {
return statusFromErrno(errno, fmt::format("Failed to get program from {}", programPath));
}
- if (android::bpf::attachProgram(type, cgroupProg, cgroupFd)) {
+ if (bpf::attachProgram(type, cgroupProg, cgroupFd)) {
return statusFromErrno(errno, fmt::format("Program {} attach failed", programPath));
}
return netdutils::status::ok;
@@ -70,50 +75,39 @@
return netdutils::status::ok;
}
-// Checks if the device is running on release version of Android 25Q2 or newer.
-static bool isAtLeast25Q2() {
- return android_get_device_api_level() >= 36 ||
- (android_get_device_api_level() == 35 &&
- modules::sdklevel::detail::IsAtLeastPreReleaseCodename("Baklava"));
-}
-
static Status initPrograms(const char* cg2_path) {
if (!cg2_path) return Status("cg2_path is NULL");
// This code was mainlined in T, so this should be trivially satisfied.
- if (!modules::sdklevel::IsAtLeastT()) return Status("S- platform is unsupported");
+ if (!isAtLeastT) return Status("S- platform is unsupported");
// S requires eBPF support which was only added in 4.9, so this should be satisfied.
- if (!bpf::isAtLeastKernelVersion(4, 9, 0)) {
+ if (!isAtLeastKernelVersion(4, 9, 0)) {
return Status("kernel version < 4.9.0 is unsupported");
}
// U bumps the kernel requirement up to 4.14
- if (modules::sdklevel::IsAtLeastU() && !bpf::isAtLeastKernelVersion(4, 14, 0)) {
+ if (isAtLeastU && !isAtLeastKernelVersion(4, 14, 0)) {
return Status("U+ platform with kernel version < 4.14.0 is unsupported");
}
// U mandates this mount point (though it should also be the case on T)
- if (modules::sdklevel::IsAtLeastU() && !!strcmp(cg2_path, "/sys/fs/cgroup")) {
+ if (isAtLeastU && !!strcmp(cg2_path, "/sys/fs/cgroup")) {
return Status("U+ platform with cg2_path != /sys/fs/cgroup is unsupported");
}
// V bumps the kernel requirement up to 4.19
- if (modules::sdklevel::IsAtLeastV() && !bpf::isAtLeastKernelVersion(4, 19, 0)) {
+ if (isAtLeastV && !isAtLeastKernelVersion(4, 19, 0)) {
return Status("V+ platform with kernel version < 4.19.0 is unsupported");
}
// 25Q2 bumps the kernel requirement up to 5.4
- if (isAtLeast25Q2() && !bpf::isAtLeastKernelVersion(5, 4, 0)) {
+ if (isAtLeast25Q2 && !isAtLeastKernelVersion(5, 4, 0)) {
return Status("25Q2+ platform with kernel version < 5.4.0 is unsupported");
}
unique_fd cg_fd(open(cg2_path, O_DIRECTORY | O_RDONLY | O_CLOEXEC));
- if (!cg_fd.ok()) {
- const int err = errno;
- ALOGE("Failed to open the cgroup directory: %s", strerror(err));
- return statusFromErrno(err, "Open the cgroup directory failed");
- }
+ if (!cg_fd.ok()) return statusFromErrno(errno, "Opening cgroup dir failed");
RETURN_IF_NOT_OK(checkProgramAccessible(XT_BPF_ALLOWLIST_PROG_PATH));
RETURN_IF_NOT_OK(checkProgramAccessible(XT_BPF_DENYLIST_PROG_PATH));
@@ -127,20 +121,20 @@
// cgroup if the program is pinned properly.
// TODO: delete the if statement once all devices should support cgroup
// socket filter (ie. the minimum kernel version required is 4.14).
- if (bpf::isAtLeastKernelVersion(4, 14, 0)) {
+ if (isAtLeastKernelVersion(4, 14, 0)) {
RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_INET_CREATE_PROG_PATH,
cg_fd, BPF_CGROUP_INET_SOCK_CREATE));
}
- if (bpf::isAtLeastKernelVersion(5, 10, 0)) {
+ if (isAtLeastKernelVersion(5, 10, 0)) {
RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_INET_RELEASE_PROG_PATH,
cg_fd, BPF_CGROUP_INET_SOCK_RELEASE));
}
- if (modules::sdklevel::IsAtLeastV()) {
+ if (isAtLeastV) {
// V requires 4.19+, so technically this 2nd 'if' is not required, but it
// doesn't hurt us to try to support AOSP forks that try to support older kernels.
- if (bpf::isAtLeastKernelVersion(4, 19, 0)) {
+ if (isAtLeastKernelVersion(4, 19, 0)) {
RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_CONNECT4_PROG_PATH,
cg_fd, BPF_CGROUP_INET4_CONNECT));
RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_CONNECT6_PROG_PATH,
@@ -155,7 +149,7 @@
cg_fd, BPF_CGROUP_UDP6_SENDMSG));
}
- if (bpf::isAtLeastKernelVersion(5, 4, 0)) {
+ if (isAtLeastKernelVersion(5, 4, 0)) {
RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_GETSOCKOPT_PROG_PATH,
cg_fd, BPF_CGROUP_GETSOCKOPT));
RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_SETSOCKOPT_PROG_PATH,
@@ -163,7 +157,7 @@
}
}
- if (bpf::isAtLeastKernelVersion(4, 19, 0)) {
+ if (isAtLeastKernelVersion(4, 19, 0)) {
RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_BIND4_PROG_PATH,
cg_fd, BPF_CGROUP_INET4_BIND));
RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_BIND6_PROG_PATH,
@@ -171,32 +165,32 @@
// This should trivially pass, since we just attached up above,
// but BPF_PROG_QUERY is only implemented on 4.19+ kernels.
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_EGRESS) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_INGRESS) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_SOCK_CREATE) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET4_BIND) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET6_BIND) <= 0) abort();
+ if (queryProgram(cg_fd, BPF_CGROUP_INET_EGRESS) <= 0) abort();
+ if (queryProgram(cg_fd, BPF_CGROUP_INET_INGRESS) <= 0) abort();
+ if (queryProgram(cg_fd, BPF_CGROUP_INET_SOCK_CREATE) <= 0) abort();
+ if (queryProgram(cg_fd, BPF_CGROUP_INET4_BIND) <= 0) abort();
+ if (queryProgram(cg_fd, BPF_CGROUP_INET6_BIND) <= 0) abort();
}
- if (bpf::isAtLeastKernelVersion(5, 10, 0)) {
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_SOCK_RELEASE) <= 0) abort();
+ if (isAtLeastKernelVersion(5, 10, 0)) {
+ if (queryProgram(cg_fd, BPF_CGROUP_INET_SOCK_RELEASE) <= 0) abort();
}
- if (modules::sdklevel::IsAtLeastV()) {
+ if (isAtLeastV) {
// V requires 4.19+, so technically this 2nd 'if' is not required, but it
// doesn't hurt us to try to support AOSP forks that try to support older kernels.
- if (bpf::isAtLeastKernelVersion(4, 19, 0)) {
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET4_CONNECT) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET6_CONNECT) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP4_RECVMSG) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP6_RECVMSG) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP4_SENDMSG) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP6_SENDMSG) <= 0) abort();
+ if (isAtLeastKernelVersion(4, 19, 0)) {
+ if (queryProgram(cg_fd, BPF_CGROUP_INET4_CONNECT) <= 0) abort();
+ if (queryProgram(cg_fd, BPF_CGROUP_INET6_CONNECT) <= 0) abort();
+ if (queryProgram(cg_fd, BPF_CGROUP_UDP4_RECVMSG) <= 0) abort();
+ if (queryProgram(cg_fd, BPF_CGROUP_UDP6_RECVMSG) <= 0) abort();
+ if (queryProgram(cg_fd, BPF_CGROUP_UDP4_SENDMSG) <= 0) abort();
+ if (queryProgram(cg_fd, BPF_CGROUP_UDP6_SENDMSG) <= 0) abort();
}
- if (bpf::isAtLeastKernelVersion(5, 4, 0)) {
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_GETSOCKOPT) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_SETSOCKOPT) <= 0) abort();
+ if (isAtLeastKernelVersion(5, 4, 0)) {
+ if (queryProgram(cg_fd, BPF_CGROUP_GETSOCKOPT) <= 0) abort();
+ if (queryProgram(cg_fd, BPF_CGROUP_SETSOCKOPT) <= 0) abort();
}
}
@@ -236,7 +230,7 @@
// but there could be platform provided (xt_)bpf programs that oem/vendor
// modified netd (which calls us during init) depends on...
ALOGI("Waiting for platform BPF programs");
- android::bpf::waitForProgsLoaded();
+ bpf::waitForProgsLoaded();
}
if (!mainlineNetBpfLoadDone()) {
@@ -268,12 +262,21 @@
// ...unless someone changed 'exec_start bpfloader' to 'start bpfloader'
// in the rc file.
//
- // TODO: should be: if (!modules::sdklevel::IsAtLeastW())
- if (android_get_device_api_level() <= __ANDROID_API_V__) waitForBpf();
+ if (!isAtLeast25Q2) waitForBpf();
RETURN_IF_NOT_OK(initPrograms(cg2_path));
RETURN_IF_NOT_OK(initMaps());
+ if (isAtLeast25Q2) {
+ // Make sure netd can create & write maps. sepolicy is V+, but enough to enforce on 25Q2+
+ int key = 1;
+ int value = 123;
+ unique_fd map(bpf::createMap(BPF_MAP_TYPE_ARRAY, sizeof(key), sizeof(value), 2, 0));
+ if (!map.ok()) return statusFromErrno(errno, fmt::format("map create failed"));
+ int rv = bpf::writeToMapEntry(map, &key, &value, BPF_ANY);
+ if (rv) return statusFromErrno(errno, fmt::format("map write failed (rv={})", rv));
+ }
+
return netdutils::status::ok;
}
@@ -300,7 +303,7 @@
Status BpfHandler::initMaps() {
// bpfLock() requires bpfGetFdMapId which is only available on 4.14+ kernels.
- if (bpf::isAtLeastKernelVersion(4, 14, 0)) {
+ if (isAtLeastKernelVersion(4, 14, 0)) {
mapLockTest();
}
@@ -310,7 +313,6 @@
RETURN_IF_NOT_OK(mUidPermissionMap.init(UID_PERMISSION_MAP_PATH));
// initialized last so mCookieTagMap.isValid() implies everything else is valid too
RETURN_IF_NOT_OK(mCookieTagMap.init(COOKIE_TAG_MAP_PATH));
- ALOGI("%s successfully", __func__);
return netdutils::status::ok;
}
@@ -339,8 +341,8 @@
if (chargeUid == AID_CLAT) return -EPERM;
// The socket destroy listener only monitors on the group {INET_TCP, INET_UDP, INET6_TCP,
- // INET6_UDP}. Tagging listener unsupported socket causes that the tag can't be removed from
- // tag map automatically. Eventually, the tag map may run out of space because of dead tag
+ // INET6_UDP}. Tagging listener unsupported sockets (on <5.10) means the tag cannot be
+ // removed from tag map automatically. Eventually, it may run out of space due to dead tag
// entries. Note that although tagSocket() of net client has already denied the family which
// is neither AF_INET nor AF_INET6, the family validation is still added here just in case.
// See tagSocket in system/netd/client/NetdClient.cpp and
@@ -354,19 +356,23 @@
return -errno;
}
if (socketFamily != AF_INET && socketFamily != AF_INET6) {
- ALOGE("Unsupported family: %d", socketFamily);
+ ALOGV("Unsupported family: %d", socketFamily);
return -EAFNOSUPPORT;
}
- int socketProto;
- socklen_t protoLen = sizeof(socketProto);
- if (getsockopt(sockFd, SOL_SOCKET, SO_PROTOCOL, &socketProto, &protoLen)) {
- ALOGE("Failed to getsockopt SO_PROTOCOL: %s, fd: %d", strerror(errno), sockFd);
- return -errno;
- }
- if (socketProto != IPPROTO_UDP && socketProto != IPPROTO_TCP) {
- ALOGE("Unsupported protocol: %d", socketProto);
- return -EPROTONOSUPPORT;
+ // On 5.10+ the BPF_CGROUP_INET_SOCK_RELEASE hook takes care of cookie tag map cleanup
+ // during socket destruction. As such the socket destroy listener is superfluous.
+ if (!isAtLeastKernelVersion(5, 10, 0)) {
+ int socketProto;
+ socklen_t protoLen = sizeof(socketProto);
+ if (getsockopt(sockFd, SOL_SOCKET, SO_PROTOCOL, &socketProto, &protoLen)) {
+ ALOGE("Failed to getsockopt SO_PROTOCOL: %s, fd: %d", strerror(errno), sockFd);
+ return -errno;
+ }
+ if (socketProto != IPPROTO_UDP && socketProto != IPPROTO_TCP) {
+ ALOGV("Unsupported protocol: %d", socketProto);
+ return -EPROTONOSUPPORT;
+ }
}
uint64_t sock_cookie = getSocketCookie(sockFd);
@@ -427,8 +433,8 @@
ALOGE("Failed to tag the socket: %s", strerror(res.error().code()));
return -res.error().code();
}
- ALOGD("Socket with cookie %" PRIu64 " tagged successfully with tag %" PRIu32 " uid %u "
- "and real uid %u", sock_cookie, tag, chargeUid, realUid);
+ ALOGV("Socket with cookie %" PRIu64 " tagged successfully with tag %" PRIu32 " uid %u "
+ "and real uid %u", sock_cookie, tag, chargeUid, realUid);
return 0;
}
@@ -439,10 +445,11 @@
if (!mCookieTagMap.isValid()) return -EPERM;
base::Result<void> res = mCookieTagMap.deleteValue(sock_cookie);
if (!res.ok()) {
- ALOGE("Failed to untag socket: %s", strerror(res.error().code()));
- return -res.error().code();
+ const int err = res.error().code();
+ if (err != ENOENT) ALOGE("Failed to untag socket: %s", strerror(err));
+ return -err;
}
- ALOGD("Socket with cookie %" PRIu64 " untagged successfully.", sock_cookie);
+ ALOGV("Socket with cookie %" PRIu64 " untagged successfully.", sock_cookie);
return 0;
}
diff --git a/bpf/netd/BpfHandlerTest.cpp b/bpf/netd/BpfHandlerTest.cpp
index b38fa16..4002b4c 100644
--- a/bpf/netd/BpfHandlerTest.cpp
+++ b/bpf/netd/BpfHandlerTest.cpp
@@ -191,7 +191,11 @@
int rawSocket = socket(AF_INET, SOCK_RAW | SOCK_CLOEXEC, IPPROTO_RAW);
EXPECT_LE(0, rawSocket);
EXPECT_NE(NONEXISTENT_COOKIE, getSocketCookie(rawSocket));
- EXPECT_EQ(-EPROTONOSUPPORT, mBh.tagSocket(rawSocket, TEST_TAG, TEST_UID, TEST_UID));
+ if (isAtLeastKernelVersion(5, 10, 0)) {
+ EXPECT_EQ(0, mBh.tagSocket(rawSocket, TEST_TAG, TEST_UID, TEST_UID));
+ } else {
+ EXPECT_EQ(-EPROTONOSUPPORT, mBh.tagSocket(rawSocket, TEST_TAG, TEST_UID, TEST_UID));
+ }
}
TEST_F(BpfHandlerTest, TestTagSocketWithoutPermission) {
diff --git a/bpf/progs/Android.bp b/bpf/progs/Android.bp
index 20d194c..2bfe613 100644
--- a/bpf/progs/Android.bp
+++ b/bpf/progs/Android.bp
@@ -69,32 +69,16 @@
sub_dir: "net_shared",
}
-// Ships to Android S, the bpfloader of which fails to parse BTF enabled .o's.
bpf {
name: "offload.o",
srcs: ["offload.c"],
- btf: false,
+ sub_dir: "tethering",
}
-// This version ships to Android T+ which uses mainline netbpfload.
-bpf {
- name: "offload@mainline.o",
- srcs: ["offload@mainline.c"],
- cflags: ["-DMAINLINE"],
-}
-
-// Ships to Android S, the bpfloader of which fails to parse BTF enabled .o's.
bpf {
name: "test.o",
srcs: ["test.c"],
- btf: false,
-}
-
-// This version ships to Android T+ which uses mainline netbpfload.
-bpf {
- name: "test@mainline.o",
- srcs: ["test@mainline.c"],
- cflags: ["-DMAINLINE"],
+ sub_dir: "tethering",
}
bpf {
diff --git a/bpf/progs/bpf_net_helpers.h b/bpf/progs/bpf_net_helpers.h
index a5664ba..4085ed4 100644
--- a/bpf/progs/bpf_net_helpers.h
+++ b/bpf/progs/bpf_net_helpers.h
@@ -84,6 +84,8 @@
#define ETH_IP6_TCP_OFFSET(field) (ETH_HLEN + IP6_TCP_OFFSET(field))
#define ETH_IP6_UDP_OFFSET(field) (ETH_HLEN + IP6_UDP_OFFSET(field))
+static uint64_t (*bpf_get_netns_cookie)(void* ctx) = (void*)BPF_FUNC_get_netns_cookie;
+
// this returns 0 iff skb->sk is NULL
static uint64_t (*bpf_get_socket_cookie)(struct __sk_buff* skb) = (void*)BPF_FUNC_get_socket_cookie;
static uint64_t (*bpf_get_sk_cookie)(struct bpf_sock* sk) = (void*)BPF_FUNC_get_socket_cookie;
diff --git a/bpf/progs/clatd.c b/bpf/progs/clatd.c
index 2d4551e..2bb9d6f 100644
--- a/bpf/progs/clatd.c
+++ b/bpf/progs/clatd.c
@@ -288,6 +288,9 @@
// We cannot handle IP options, just standard 20 byte == 5 dword minimal IPv4 header
if (ip4->ihl != 5) return TC_ACT_PIPE;
+ // Packet must not be multicast
+ if ((ip4->daddr & 0xf0000000) == 0xe0000000) return TC_ACT_PIPE;
+
// Calculate the IPv4 one's complement checksum of the IPv4 header.
__wsum sum4 = 0;
for (unsigned i = 0; i < sizeof(*ip4) / sizeof(__u16); ++i) {
diff --git a/bpf/progs/netd.c b/bpf/progs/netd.c
index 9244350..08635b3 100644
--- a/bpf/progs/netd.c
+++ b/bpf/progs/netd.c
@@ -25,10 +25,6 @@
static const int PASS = 1;
static const int DROP_UNLESS_DNS = 2; // internal to our program
-// Used for 'bool enable_tracing'
-static const bool TRACE_ON = true;
-static const bool TRACE_OFF = false;
-
// offsetof(struct iphdr, ihl) -- but that's a bitfield
#define IPPROTO_IHL_OFF 0
@@ -103,6 +99,17 @@
DEFINE_BPF_MAP_RO_NETD(data_saver_enabled_map, ARRAY, uint32_t, bool,
DATA_SAVER_ENABLED_MAP_SIZE)
+DEFINE_BPF_MAP_EXT(local_net_access_map, LPM_TRIE, LocalNetAccessKey, bool, 1000,
+ AID_ROOT, AID_NET_BW_ACCT, 0060, "fs_bpf_net_shared", "", PRIVATE,
+ BPFLOADER_MAINLINE_25Q2_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG, LOAD_ON_USER,
+ LOAD_ON_USERDEBUG, 0)
+
+// not preallocated
+DEFINE_BPF_MAP_EXT(local_net_blocked_uid_map, HASH, uint32_t, bool, -1000,
+ AID_ROOT, AID_NET_BW_ACCT, 0060, "fs_bpf_net_shared", "", PRIVATE,
+ BPFLOADER_MAINLINE_25Q2_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG, LOAD_ON_USER,
+ LOAD_ON_USERDEBUG, 0)
+
// iptables xt_bpf programs need to be usable by both netd and netutils_wrappers
// selinux contexts, because even non-xt_bpf iptables mutations are implemented as
// a full table dump, followed by an update in userspace, and then a reload into the kernel,
@@ -110,31 +117,34 @@
// program (see XT_BPF_MODE_PATH_PINNED) and then the iptables binary (or rather
// the kernel acting on behalf of it) must be able to retrieve the pinned program
// for the reload to succeed
-#define DEFINE_XTBPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
- DEFINE_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog)
+#define DEFINE_XTBPF_PROG(SECTION_NAME, the_prog) \
+ DEFINE_BPF_PROG(SECTION_NAME, AID_ROOT, AID_NET_ADMIN, the_prog)
// programs that need to be usable by netd, but not by netutils_wrappers
// (this is because these are currently attached by the mainline provided libnetd_updatable .so
// which is loaded into netd and thus runs as netd uid/gid/selinux context)
-#define DEFINE_NETD_BPF_PROG_KVER_RANGE(SECTION_NAME, prog_uid, prog_gid, the_prog, minKV, maxKV) \
- DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, \
- minKV, maxKV, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, MANDATORY, \
+#define DEFINE_NETD_BPF_PROG_RANGES(SECTION_NAME, the_prog, minKV, maxKV, min_loader, max_loader) \
+ DEFINE_BPF_PROG_EXT(SECTION_NAME, AID_ROOT, AID_ROOT, the_prog, \
+ minKV, maxKV, min_loader, max_loader, MANDATORY, \
"fs_bpf_netd_readonly", "", LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
-#define DEFINE_NETD_BPF_PROG_KVER(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv) \
- DEFINE_NETD_BPF_PROG_KVER_RANGE(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, KVER_INF)
+#define DEFINE_NETD_BPF_PROG_KVER_RANGE(SECTION_NAME, the_prog, minKV, maxKV) \
+ DEFINE_NETD_BPF_PROG_RANGES(SECTION_NAME, the_prog, minKV, maxKV, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER)
-#define DEFINE_NETD_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
- DEFINE_NETD_BPF_PROG_KVER(SECTION_NAME, prog_uid, prog_gid, the_prog, KVER_NONE)
+#define DEFINE_NETD_BPF_PROG_KVER(SECTION_NAME, the_prog, min_kv) \
+ DEFINE_NETD_BPF_PROG_KVER_RANGE(SECTION_NAME, the_prog, min_kv, KVER_INF)
-#define DEFINE_NETD_V_BPF_PROG_KVER(SECTION_NAME, prog_uid, prog_gid, the_prog, minKV) \
- DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, minKV, \
+#define DEFINE_NETD_BPF_PROG(SECTION_NAME, the_prog) \
+ DEFINE_NETD_BPF_PROG_KVER(SECTION_NAME, the_prog, KVER_NONE)
+
+#define DEFINE_NETD_V_BPF_PROG_KVER(SECTION_NAME, the_prog, minKV) \
+ DEFINE_BPF_PROG_EXT(SECTION_NAME, AID_ROOT, AID_ROOT, the_prog, minKV, \
KVER_INF, BPFLOADER_MAINLINE_V_VERSION, BPFLOADER_MAX_VER, MANDATORY, \
"fs_bpf_netd_readonly", "", LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
// programs that only need to be usable by the system server
-#define DEFINE_SYS_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
- DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, KVER_NONE, KVER_INF, \
+#define DEFINE_SYS_BPF_PROG(SECTION_NAME, the_prog) \
+ DEFINE_BPF_PROG_EXT(SECTION_NAME, AID_ROOT, AID_NET_ADMIN, the_prog, KVER_NONE, KVER_INF, \
BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, MANDATORY, \
"fs_bpf_net_shared", "", LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
@@ -231,11 +241,70 @@
: bpf_skb_load_bytes(skb, L3_off, to, len);
}
+// False iff arguments are found with longest prefix match lookup and disallowed.
+static inline __always_inline bool is_local_net_access_allowed(const uint32_t if_index,
+ const struct in6_addr* remote_ip6, const uint16_t protocol, const __be16 remote_port) {
+ LocalNetAccessKey query_key = {
+ .lpm_bitlen = 8 * (sizeof(if_index) + sizeof(*remote_ip6) + sizeof(protocol)
+ + sizeof(remote_port)),
+ .if_index = if_index,
+ .remote_ip6 = *remote_ip6,
+ .protocol = protocol,
+ .remote_port = remote_port
+ };
+ bool* v = bpf_local_net_access_map_lookup_elem(&query_key);
+ return v ? *v : true;
+}
+
+static __always_inline inline bool should_block_local_network_packets(struct __sk_buff *skb,
+ const uint32_t uid, const struct egress_bool egress,
+ const struct kver_uint kver) {
+ if (is_system_uid(uid)) return false;
+
+ bool* block_local_net = bpf_local_net_blocked_uid_map_lookup_elem(&uid);
+ if (!block_local_net) return false; // uid not found in map
+ if (!*block_local_net) return false; // lookup returned 'bool false'
+
+ struct in6_addr remote_ip6;
+ uint8_t ip_proto;
+ uint8_t L4_off;
+ if (skb->protocol == htons(ETH_P_IP)) {
+ int remote_ip_ofs = egress.egress ? IP4_OFFSET(daddr) : IP4_OFFSET(saddr);
+ remote_ip6.s6_addr32[0] = 0;
+ remote_ip6.s6_addr32[1] = 0;
+ remote_ip6.s6_addr32[2] = htonl(0xFFFF);
+ (void)bpf_skb_load_bytes_net(skb, remote_ip_ofs, &remote_ip6.s6_addr32[3], 4, kver);
+ (void)bpf_skb_load_bytes_net(skb, IP4_OFFSET(protocol), &ip_proto, sizeof(ip_proto), kver);
+ uint8_t ihl;
+ (void)bpf_skb_load_bytes_net(skb, IPPROTO_IHL_OFF, &ihl, sizeof(ihl), kver);
+ L4_off = (ihl & 0x0F) * 4; // IHL calculation.
+ } else if (skb->protocol == htons(ETH_P_IPV6)) {
+ int remote_ip_ofs = egress.egress ? IP6_OFFSET(daddr) : IP6_OFFSET(saddr);
+ (void)bpf_skb_load_bytes_net(skb, remote_ip_ofs, &remote_ip6, sizeof(remote_ip6), kver);
+ (void)bpf_skb_load_bytes_net(skb, IP6_OFFSET(nexthdr), &ip_proto, sizeof(ip_proto), kver);
+ L4_off = sizeof(struct ipv6hdr);
+ } else {
+ return false;
+ }
+
+ __be16 remote_port = 0;
+ switch (ip_proto) {
+ case IPPROTO_TCP:
+ case IPPROTO_DCCP:
+ case IPPROTO_UDP:
+ case IPPROTO_UDPLITE:
+ case IPPROTO_SCTP:
+ (void)bpf_skb_load_bytes_net(skb, L4_off + (egress.egress ? 2 : 0), &remote_port, sizeof(remote_port), kver);
+ break;
+ }
+
+ return !is_local_net_access_allowed(skb->ifindex, &remote_ip6, ip_proto, remote_port);
+}
+
static __always_inline inline void do_packet_tracing(
const struct __sk_buff* const skb, const struct egress_bool egress, const uint32_t uid,
- const uint32_t tag, const bool enable_tracing, const struct kver_uint kver) {
- if (!enable_tracing) return;
- if (!KVER_IS_AT_LEAST(kver, 5, 8, 0)) return;
+ const uint32_t tag, const struct kver_uint kver) {
+ if (!KVER_IS_AT_LEAST(kver, 5, 10, 0)) return;
uint32_t mapKey = 0;
bool* traceConfig = bpf_packet_trace_enabled_map_lookup_elem(&mapKey);
@@ -393,7 +462,8 @@
static __always_inline inline int bpf_owner_match(struct __sk_buff* skb, uint32_t uid,
const struct egress_bool egress,
- const struct kver_uint kver) {
+ const struct kver_uint kver,
+ const struct sdk_level_uint lvl) {
if (is_system_uid(uid)) return PASS;
if (skip_owner_match(skb, egress, kver)) return PASS;
@@ -423,6 +493,11 @@
return DROP_UNLESS_DNS;
}
}
+
+ if (SDK_LEVEL_IS_AT_LEAST(lvl, 25Q2) && skb->ifindex == 1) {
+ // TODO: sdksandbox localhost restrictions
+ }
+
return PASS;
}
@@ -440,8 +515,8 @@
static __always_inline inline int bpf_traffic_account(struct __sk_buff* skb,
const struct egress_bool egress,
- const bool enable_tracing,
- const struct kver_uint kver) {
+ const struct kver_uint kver,
+ const struct sdk_level_uint lvl) {
// sock_uid will be 'overflowuid' if !sk_fullsock(sk_to_full_sk(skb->sk))
uint32_t sock_uid = bpf_get_socket_uid(skb);
@@ -470,7 +545,7 @@
// CLAT daemon receives via an untagged AF_PACKET socket.
if (egress.egress && uid == AID_CLAT) return PASS;
- int match = bpf_owner_match(skb, sock_uid, egress, kver);
+ int match = bpf_owner_match(skb, sock_uid, egress, kver, lvl);
// Workaround for secureVPN with VpnIsolation enabled, refer to b/159994981 for details.
// Keep TAG_SYSTEM_DNS in sync with DnsResolver/include/netd_resolv/resolv.h
@@ -483,6 +558,10 @@
if (match == DROP_UNLESS_DNS) match = DROP;
}
+ if (SDK_LEVEL_IS_AT_LEAST(lvl, 25Q2) && (match != DROP)) {
+ if (should_block_local_network_packets(skb, uid, egress, kver)) match = DROP;
+ }
+
// If an outbound packet is going to be dropped, we do not count that traffic.
if (egress.egress && (match == DROP)) return DROP;
@@ -496,7 +575,7 @@
if (!selectedMap) return PASS; // cannot happen, needed to keep bpf verifier happy
- do_packet_tracing(skb, egress, uid, tag, enable_tracing, kver);
+ do_packet_tracing(skb, egress, uid, tag, kver);
update_stats_with_config(*selectedMap, skb, &key, egress, kver);
update_app_uid_stats_map(skb, &uid, egress, kver);
@@ -509,52 +588,104 @@
return match;
}
-// Tracing on Android U+ 5.8+
-DEFINE_BPF_PROG_EXT("cgroupskb/ingress/stats$trace", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_ingress_trace, KVER_5_8, KVER_INF,
- BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, MANDATORY,
- "fs_bpf_netd_readonly", "",
- LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+// -----
+
+// Supported kernel + platform/os version combinations:
+//
+// | 4.9 | 4.14 | 4.19 | 5.4 | 5.10 | 5.15 | 6.1 | 6.6 | 6.12 |
+// 25Q2 | | | | x | x | x | x | x | x |
+// V | | | x | x | x | x | x | x | | (netbpfload)
+// U | | x | x | x | x | x | x | | |
+// T | x | x | x | x | x | x | | | | (magic netbpfload)
+// S | x | x | x | x | x | | | | | (platform loads offload)
+// R | x | x | x | x | | | | | | (no mainline ebpf)
+//
+// Not relevant for eBPF, but R can also run on 4.4
+
+// ----- cgroupskb/ingress/stats -----
+
+// Android 25Q2+ 5.10+ (localnet protection + tracing)
+DEFINE_NETD_BPF_PROG_RANGES("cgroupskb/ingress/stats$5_10_25q2",
+ bpf_cgroup_ingress_5_10_25q2, KVER_5_10, KVER_INF,
+ BPFLOADER_MAINLINE_25Q2_VERSION, BPFLOADER_MAX_VER)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, INGRESS, TRACE_ON, KVER_5_8);
+ return bpf_traffic_account(skb, INGRESS, KVER_5_10, SDK_LEVEL_25Q2);
}
-DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/ingress/stats$4_19", AID_ROOT, AID_SYSTEM,
+// Android 25Q2+ 5.4 (localnet protection)
+DEFINE_NETD_BPF_PROG_RANGES("cgroupskb/ingress/stats$5_4_25q2",
+ bpf_cgroup_ingress_5_4_25q2, KVER_5_4, KVER_5_10,
+ BPFLOADER_MAINLINE_25Q2_VERSION, BPFLOADER_MAX_VER)
+(struct __sk_buff* skb) {
+ return bpf_traffic_account(skb, INGRESS, KVER_5_4, SDK_LEVEL_25Q2);
+}
+
+// Android U/V 5.10+ (tracing)
+DEFINE_NETD_BPF_PROG_RANGES("cgroupskb/ingress/stats$5_10_u",
+ bpf_cgroup_ingress_5_10_u, KVER_5_10, KVER_INF,
+ BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAINLINE_25Q2_VERSION)
+(struct __sk_buff* skb) {
+ return bpf_traffic_account(skb, INGRESS, KVER_5_10, SDK_LEVEL_U);
+}
+
+// Android T/U/V 4.19 & T/U/V/25Q2 5.4 & T 5.10/5.15
+DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/ingress/stats$4_19",
bpf_cgroup_ingress_4_19, KVER_4_19, KVER_INF)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, INGRESS, TRACE_OFF, KVER_4_19);
+ return bpf_traffic_account(skb, INGRESS, KVER_4_19, SDK_LEVEL_T);
}
-DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/ingress/stats$4_14", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_ingress_4_14, KVER_NONE, KVER_4_19)
+// Android T 4.9 & T/U 4.14
+DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/ingress/stats$4_9",
+ bpf_cgroup_ingress_4_9, KVER_NONE, KVER_4_19)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, INGRESS, TRACE_OFF, KVER_NONE);
+ return bpf_traffic_account(skb, INGRESS, KVER_NONE, SDK_LEVEL_T);
}
-// Tracing on Android U+ 5.8+
-DEFINE_BPF_PROG_EXT("cgroupskb/egress/stats$trace", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_egress_trace, KVER_5_8, KVER_INF,
- BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, MANDATORY,
- "fs_bpf_netd_readonly", "",
- LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+// ----- cgroupskb/egress/stats -----
+
+// Android 25Q2+ 5.10+ (localnet protection + tracing)
+DEFINE_NETD_BPF_PROG_RANGES("cgroupskb/egress/stats$5_10_25q2",
+ bpf_cgroup_egress_5_10_25q2, KVER_5_10, KVER_INF,
+ BPFLOADER_MAINLINE_25Q2_VERSION, BPFLOADER_MAX_VER)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER_5_8);
+ return bpf_traffic_account(skb, EGRESS, KVER_5_10, SDK_LEVEL_25Q2);
}
-DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/egress/stats$4_19", AID_ROOT, AID_SYSTEM,
+// Android 25Q2+ 5.4 (localnet protection)
+DEFINE_NETD_BPF_PROG_RANGES("cgroupskb/egress/stats$5_4_25q2",
+ bpf_cgroup_egress_5_4_25q2, KVER_5_4, KVER_5_10,
+ BPFLOADER_MAINLINE_25Q2_VERSION, BPFLOADER_MAX_VER)
+(struct __sk_buff* skb) {
+ return bpf_traffic_account(skb, EGRESS, KVER_5_4, SDK_LEVEL_25Q2);
+}
+
+// Android U/V 5.10+ (tracing)
+DEFINE_NETD_BPF_PROG_RANGES("cgroupskb/egress/stats$5_10_u",
+ bpf_cgroup_egress_5_10_u, KVER_5_10, KVER_INF,
+ BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAINLINE_25Q2_VERSION)
+(struct __sk_buff* skb) {
+ return bpf_traffic_account(skb, EGRESS, KVER_5_10, SDK_LEVEL_U);
+}
+
+// Android T/U/V 4.19 & T/U/V/25Q2 5.4 & T 5.10/5.15
+DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/egress/stats$4_19",
bpf_cgroup_egress_4_19, KVER_4_19, KVER_INF)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, EGRESS, TRACE_OFF, KVER_4_19);
+ return bpf_traffic_account(skb, EGRESS, KVER_4_19, SDK_LEVEL_T);
}
-DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/egress/stats$4_14", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_egress_4_14, KVER_NONE, KVER_4_19)
+// Android T 4.9 & T/U 4.14
+DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/egress/stats$4_9",
+ bpf_cgroup_egress_4_9, KVER_NONE, KVER_4_19)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, EGRESS, TRACE_OFF, KVER_NONE);
+ return bpf_traffic_account(skb, EGRESS, KVER_NONE, SDK_LEVEL_T);
}
+// -----
+
// WARNING: Android T's non-updatable netd depends on the name of this program.
-DEFINE_XTBPF_PROG("skfilter/egress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_egress_prog)
+DEFINE_XTBPF_PROG("skfilter/egress/xtbpf", xt_bpf_egress_prog)
(struct __sk_buff* skb) {
// Clat daemon does not generate new traffic, all its traffic is accounted for already
// on the v4-* interfaces (except for the 20 (or 28) extra bytes of IPv6 vs IPv4 overhead,
@@ -573,7 +704,7 @@
}
// WARNING: Android T's non-updatable netd depends on the name of this program.
-DEFINE_XTBPF_PROG("skfilter/ingress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_ingress_prog)
+DEFINE_XTBPF_PROG("skfilter/ingress/xtbpf", xt_bpf_ingress_prog)
(struct __sk_buff* skb) {
// Clat daemon traffic is not accounted by virtue of iptables raw prerouting drop rule
// (in clat_raw_PREROUTING chain), which triggers before this (in bw_raw_PREROUTING chain).
@@ -585,7 +716,7 @@
return XTBPF_MATCH;
}
-DEFINE_SYS_BPF_PROG("schedact/ingress/account", AID_ROOT, AID_NET_ADMIN,
+DEFINE_SYS_BPF_PROG("schedact/ingress/account",
tc_bpf_ingress_account_prog)
(struct __sk_buff* skb) {
if (is_received_skb(skb)) {
@@ -597,7 +728,7 @@
}
// WARNING: Android T's non-updatable netd depends on the name of this program.
-DEFINE_XTBPF_PROG("skfilter/allowlist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_allowlist_prog)
+DEFINE_XTBPF_PROG("skfilter/allowlist/xtbpf", xt_bpf_allowlist_prog)
(struct __sk_buff* skb) {
uint32_t sock_uid = bpf_get_socket_uid(skb);
if (is_system_uid(sock_uid)) return XTBPF_MATCH;
@@ -616,7 +747,7 @@
}
// WARNING: Android T's non-updatable netd depends on the name of this program.
-DEFINE_XTBPF_PROG("skfilter/denylist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_denylist_prog)
+DEFINE_XTBPF_PROG("skfilter/denylist/xtbpf", xt_bpf_denylist_prog)
(struct __sk_buff* skb) {
uint32_t sock_uid = bpf_get_socket_uid(skb);
UidOwnerValue* denylistMatch = bpf_uid_owner_map_lookup_elem(&sock_uid);
@@ -639,14 +770,12 @@
return permissions ? *permissions : BPF_PERMISSION_INTERNET;
}
-DEFINE_NETD_BPF_PROG_KVER("cgroupsock/inet_create", AID_ROOT, AID_ROOT, inet_socket_create,
- KVER_4_14)
+DEFINE_NETD_BPF_PROG_KVER("cgroupsock/inet_create", inet_socket_create, KVER_4_14)
(__unused struct bpf_sock* sk) {
return (get_app_permissions() & BPF_PERMISSION_INTERNET) ? BPF_ALLOW : BPF_DISALLOW;
}
-DEFINE_NETD_BPF_PROG_KVER("cgroupsockrelease/inet_release", AID_ROOT, AID_ROOT,
- inet_socket_release, KVER_5_10)
+DEFINE_NETD_BPF_PROG_KVER("cgroupsockrelease/inet_release", inet_socket_release, KVER_5_10)
(struct bpf_sock* sk) {
uint64_t cookie = bpf_get_sk_cookie(sk);
if (cookie) bpf_cookie_tag_map_delete_elem(&cookie);
@@ -699,47 +828,47 @@
return BPF_ALLOW;
}
-DEFINE_NETD_BPF_PROG_KVER("bind4/inet4_bind", AID_ROOT, AID_ROOT, inet4_bind, KVER_4_19)
+DEFINE_NETD_BPF_PROG_KVER("bind4/inet4_bind", inet4_bind, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return block_port(ctx);
}
-DEFINE_NETD_BPF_PROG_KVER("bind6/inet6_bind", AID_ROOT, AID_ROOT, inet6_bind, KVER_4_19)
+DEFINE_NETD_BPF_PROG_KVER("bind6/inet6_bind", inet6_bind, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return block_port(ctx);
}
-DEFINE_NETD_V_BPF_PROG_KVER("connect4/inet4_connect", AID_ROOT, AID_ROOT, inet4_connect, KVER_4_19)
+DEFINE_NETD_V_BPF_PROG_KVER("connect4/inet4_connect", inet4_connect, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return check_localhost(ctx);
}
-DEFINE_NETD_V_BPF_PROG_KVER("connect6/inet6_connect", AID_ROOT, AID_ROOT, inet6_connect, KVER_4_19)
+DEFINE_NETD_V_BPF_PROG_KVER("connect6/inet6_connect", inet6_connect, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return check_localhost(ctx);
}
-DEFINE_NETD_V_BPF_PROG_KVER("recvmsg4/udp4_recvmsg", AID_ROOT, AID_ROOT, udp4_recvmsg, KVER_4_19)
+DEFINE_NETD_V_BPF_PROG_KVER("recvmsg4/udp4_recvmsg", udp4_recvmsg, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return check_localhost(ctx);
}
-DEFINE_NETD_V_BPF_PROG_KVER("recvmsg6/udp6_recvmsg", AID_ROOT, AID_ROOT, udp6_recvmsg, KVER_4_19)
+DEFINE_NETD_V_BPF_PROG_KVER("recvmsg6/udp6_recvmsg", udp6_recvmsg, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return check_localhost(ctx);
}
-DEFINE_NETD_V_BPF_PROG_KVER("sendmsg4/udp4_sendmsg", AID_ROOT, AID_ROOT, udp4_sendmsg, KVER_4_19)
+DEFINE_NETD_V_BPF_PROG_KVER("sendmsg4/udp4_sendmsg", udp4_sendmsg, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return check_localhost(ctx);
}
-DEFINE_NETD_V_BPF_PROG_KVER("sendmsg6/udp6_sendmsg", AID_ROOT, AID_ROOT, udp6_sendmsg, KVER_4_19)
+DEFINE_NETD_V_BPF_PROG_KVER("sendmsg6/udp6_sendmsg", udp6_sendmsg, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return check_localhost(ctx);
}
-DEFINE_NETD_V_BPF_PROG_KVER("getsockopt/prog", AID_ROOT, AID_ROOT, getsockopt_prog, KVER_5_4)
+DEFINE_NETD_V_BPF_PROG_KVER("getsockopt/prog", getsockopt_prog, KVER_5_4)
(struct bpf_sockopt *ctx) {
// Tell kernel to return 'original' kernel reply (instead of the bpf modified buffer)
// This is important if the answer is larger than PAGE_SIZE (max size this bpf hook can provide)
@@ -747,7 +876,7 @@
return BPF_ALLOW;
}
-DEFINE_NETD_V_BPF_PROG_KVER("setsockopt/prog", AID_ROOT, AID_ROOT, setsockopt_prog, KVER_5_4)
+DEFINE_NETD_V_BPF_PROG_KVER("setsockopt/prog", setsockopt_prog, KVER_5_4)
(struct bpf_sockopt *ctx) {
// Tell kernel to use/process original buffer provided by userspace.
// This is important if it is larger than PAGE_SIZE (max size this bpf hook can handle).
diff --git a/bpf/progs/netd.h b/bpf/progs/netd.h
index be7c311..8400679 100644
--- a/bpf/progs/netd.h
+++ b/bpf/progs/netd.h
@@ -185,6 +185,8 @@
#define PACKET_TRACE_RINGBUF_PATH BPF_NETD_PATH "map_netd_packet_trace_ringbuf"
#define PACKET_TRACE_ENABLED_MAP_PATH BPF_NETD_PATH "map_netd_packet_trace_enabled_map"
#define DATA_SAVER_ENABLED_MAP_PATH BPF_NETD_PATH "map_netd_data_saver_enabled_map"
+#define LOCAL_NET_ACCESS_MAP_PATH BPF_NETD_PATH "map_netd_local_net_access_map"
+#define LOCAL_NET_BLOCKED_UID_MAP_PATH BPF_NETD_PATH "map_netd_local_net_blocked_uid_map"
#endif // __cplusplus
@@ -245,6 +247,18 @@
} IngressDiscardValue;
STRUCT_SIZE(IngressDiscardValue, 2 * 4); // 8
+typedef struct {
+ // Longest prefix match length in bits (value from 0 to 192).
+ uint32_t lpm_bitlen;
+ uint32_t if_index;
+ // IPv4 uses IPv4-mapped IPv6 address format.
+ struct in6_addr remote_ip6;
+ // u16 instead of u8 to avoid padding due to alignment requirement.
+ uint16_t protocol;
+ __be16 remote_port;
+} LocalNetAccessKey;
+STRUCT_SIZE(LocalNetAccessKey, 4 + 4 + 16 + 2 + 2); // 28
+
// Entry in the configuration map that stores which UID rules are enabled.
#define UID_RULES_CONFIGURATION_KEY 0
// Entry in the configuration map that stores which stats map is currently in use.
diff --git a/bpf/progs/offload.c b/bpf/progs/offload.c
index 631908a..b34fe6f 100644
--- a/bpf/progs/offload.c
+++ b/bpf/progs/offload.c
@@ -14,16 +14,8 @@
* limitations under the License.
*/
-#ifdef MAINLINE
-// BTF is incompatible with bpfloaders < v0.10, hence for S (v0.2) we must
-// ship a different file than for later versions, but we need bpfloader v0.25+
-// for obj@ver.o support
-#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
-#else /* MAINLINE */
-// The resulting .o needs to load on the Android S bpfloader
-#define BPFLOADER_MIN_VER BPFLOADER_S_VERSION
-#define BPFLOADER_MAX_VER BPFLOADER_T_VERSION
-#endif /* MAINLINE */
+// The resulting .o needs to load on Android S+
+#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_S_VERSION
#include "bpf_net_helpers.h"
#include "offload.h"
@@ -609,27 +601,27 @@
// Full featured (required) implementations for 5.8+ kernels (these are S+ by definition)
DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_rawip$5_8", AID_ROOT, AID_NETWORK_STACK,
- sched_cls_tether_downstream4_rawip_5_8, KVER_5_8)
+ sched_cls_tether_downstream4_rawip_5_8, KVER_5_10)
(struct __sk_buff* skb) {
- return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER_5_8);
+ return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER_5_10);
}
DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_rawip$5_8", AID_ROOT, AID_NETWORK_STACK,
- sched_cls_tether_upstream4_rawip_5_8, KVER_5_8)
+ sched_cls_tether_upstream4_rawip_5_8, KVER_5_10)
(struct __sk_buff* skb) {
- return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER_5_8);
+ return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER_5_10);
}
DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_ether$5_8", AID_ROOT, AID_NETWORK_STACK,
- sched_cls_tether_downstream4_ether_5_8, KVER_5_8)
+ sched_cls_tether_downstream4_ether_5_8, KVER_5_10)
(struct __sk_buff* skb) {
- return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER_5_8);
+ return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER_5_10);
}
DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_ether$5_8", AID_ROOT, AID_NETWORK_STACK,
- sched_cls_tether_upstream4_ether_5_8, KVER_5_8)
+ sched_cls_tether_upstream4_ether_5_8, KVER_5_10)
(struct __sk_buff* skb) {
- return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER_5_8);
+ return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER_5_10);
}
// Full featured (optional) implementations for 4.14-S, 4.19-S & 5.4-S kernels
@@ -638,7 +630,7 @@
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$opt",
AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_downstream4_rawip_opt,
- KVER_4_14, KVER_5_8)
+ KVER_4_14, KVER_5_10)
(struct __sk_buff* skb) {
return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER_4_14);
}
@@ -646,7 +638,7 @@
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$opt",
AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_upstream4_rawip_opt,
- KVER_4_14, KVER_5_8)
+ KVER_4_14, KVER_5_10)
(struct __sk_buff* skb) {
return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER_4_14);
}
@@ -654,7 +646,7 @@
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$opt",
AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_downstream4_ether_opt,
- KVER_4_14, KVER_5_8)
+ KVER_4_14, KVER_5_10)
(struct __sk_buff* skb) {
return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER_4_14);
}
@@ -662,7 +654,7 @@
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$opt",
AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_upstream4_ether_opt,
- KVER_4_14, KVER_5_8)
+ KVER_4_14, KVER_5_10)
(struct __sk_buff* skb) {
return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER_4_14);
}
@@ -682,13 +674,13 @@
// RAWIP: Required for 5.4-R kernels -- which always support bpf_skb_change_head().
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$5_4", AID_ROOT, AID_NETWORK_STACK,
- sched_cls_tether_downstream4_rawip_5_4, KVER_5_4, KVER_5_8)
+ sched_cls_tether_downstream4_rawip_5_4, KVER_5_4, KVER_5_10)
(struct __sk_buff* skb) {
return do_forward4(skb, RAWIP, DOWNSTREAM, NO_UPDATETIME, KVER_5_4);
}
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$5_4", AID_ROOT, AID_NETWORK_STACK,
- sched_cls_tether_upstream4_rawip_5_4, KVER_5_4, KVER_5_8)
+ sched_cls_tether_upstream4_rawip_5_4, KVER_5_4, KVER_5_10)
(struct __sk_buff* skb) {
return do_forward4(skb, RAWIP, UPSTREAM, NO_UPDATETIME, KVER_5_4);
}
@@ -715,13 +707,13 @@
// ETHER: Required for 4.14-Q/R, 4.19-Q/R & 5.4-R kernels.
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$4_14", AID_ROOT, AID_NETWORK_STACK,
- sched_cls_tether_downstream4_ether_4_14, KVER_4_14, KVER_5_8)
+ sched_cls_tether_downstream4_ether_4_14, KVER_4_14, KVER_5_10)
(struct __sk_buff* skb) {
return do_forward4(skb, ETHER, DOWNSTREAM, NO_UPDATETIME, KVER_4_14);
}
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$4_14", AID_ROOT, AID_NETWORK_STACK,
- sched_cls_tether_upstream4_ether_4_14, KVER_4_14, KVER_5_8)
+ sched_cls_tether_upstream4_ether_4_14, KVER_4_14, KVER_5_10)
(struct __sk_buff* skb) {
return do_forward4(skb, ETHER, UPSTREAM, NO_UPDATETIME, KVER_4_14);
}
@@ -805,7 +797,7 @@
}
#define DEFINE_XDP_PROG(str, func) \
- DEFINE_BPF_PROG_KVER(str, AID_ROOT, AID_NETWORK_STACK, func, KVER_5_9)(struct xdp_md *ctx)
+ DEFINE_BPF_PROG_KVER(str, AID_ROOT, AID_NETWORK_STACK, func, KVER_5_10)(struct xdp_md *ctx)
DEFINE_XDP_PROG("xdp/tether_downstream_ether",
xdp_tether_downstream_ether) {
diff --git a/bpf/progs/test.c b/bpf/progs/test.c
index 8585118..4dba6b9 100644
--- a/bpf/progs/test.c
+++ b/bpf/progs/test.c
@@ -14,16 +14,8 @@
* limitations under the License.
*/
-#ifdef MAINLINE
-// BTF is incompatible with bpfloaders < v0.10, hence for S (v0.2) we must
-// ship a different file than for later versions, but we need bpfloader v0.25+
-// for obj@ver.o support
-#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
-#else /* MAINLINE */
-// The resulting .o needs to load on the Android S bpfloader
-#define BPFLOADER_MIN_VER BPFLOADER_S_VERSION
-#define BPFLOADER_MAX_VER BPFLOADER_T_VERSION
-#endif /* MAINLINE */
+// The resulting .o needs to load on Android S+
+#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_S_VERSION
// This is non production code, only used for testing
// Needed because the bitmap array definition is non-kosher for pre-T OS devices.
diff --git a/bpf/syscall_wrappers/include/BpfSyscallWrappers.h b/bpf/syscall_wrappers/include/BpfSyscallWrappers.h
index a31445a..1d72b77 100644
--- a/bpf/syscall_wrappers/include/BpfSyscallWrappers.h
+++ b/bpf/syscall_wrappers/include/BpfSyscallWrappers.h
@@ -269,7 +269,7 @@
return info.FIELD; \
}
-// All 7 of these fields are already present in Linux v4.14 (even ACK 4.14-P)
+// All 9 of these fields are already present in Linux v4.14 (even ACK 4.14-P)
// while BPF_OBJ_GET_INFO_BY_FD is not implemented at all in v4.9 (even ACK 4.9-Q)
DEFINE_BPF_GET_FD(map, MapType, type) // int bpfGetFdMapType(const borrowed_fd& map_fd)
DEFINE_BPF_GET_FD(map, MapId, id) // int bpfGetFdMapId(const borrowed_fd& map_fd)
@@ -278,6 +278,8 @@
DEFINE_BPF_GET_FD(map, MaxEntries, max_entries) // int bpfGetFdMaxEntries(const borrowed_fd& map_fd)
DEFINE_BPF_GET_FD(map, MapFlags, map_flags) // int bpfGetFdMapFlags(const borrowed_fd& map_fd)
DEFINE_BPF_GET_FD(prog, ProgId, id) // int bpfGetFdProgId(const borrowed_fd& prog_fd)
+DEFINE_BPF_GET_FD(prog, JitProgLen, jited_prog_len) // int bpfGetFdJitProgLen(...)
+DEFINE_BPF_GET_FD(prog, XlatProgLen, xlated_prog_len) // int bpfGetFdXlatProgLen(...)
#undef DEFINE_BPF_GET_FD
diff --git a/bpf/tests/mts/bpf_existence_test.cpp b/bpf/tests/mts/bpf_existence_test.cpp
index 0b5b7be..4d5f9b5 100644
--- a/bpf/tests/mts/bpf_existence_test.cpp
+++ b/bpf/tests/mts/bpf_existence_test.cpp
@@ -20,7 +20,8 @@
#include <set>
#include <string>
-#include <android-modules-utils/sdk_level.h>
+#include <android-base/properties.h>
+#include <android/api-level.h>
#include <bpf/BpfUtils.h>
#include <gtest/gtest.h>
@@ -30,11 +31,12 @@
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;
+using android::bpf::isAtLeastR;
+using android::bpf::isAtLeastS;
+using android::bpf::isAtLeastT;
+using android::bpf::isAtLeastU;
+using android::bpf::isAtLeastV;
+using android::bpf::isAtLeast25Q2;
#define PLATFORM "/sys/fs/bpf/"
#define TETHERING "/sys/fs/bpf/tethering/"
@@ -159,6 +161,12 @@
NETD "prog_netd_setsockopt_prog",
};
+// Provided by *current* mainline module for 25Q2+ devices
+static const set<string> MAINLINE_FOR_25Q2_PLUS = {
+ NETD "map_netd_local_net_access_map",
+ NETD "map_netd_local_net_blocked_uid_map",
+};
+
static void addAll(set<string>& a, const set<string>& b) {
a.insert(b.begin(), b.end());
}
@@ -181,33 +189,41 @@
// 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));
+
+ // on S without a new enough DnsResolver apex, NetBpfLoad doesn't get triggered,
+ // and thus no mainline programs get loaded.
+ bool mainlineBpfCapableResolve = !access("/apex/com.android.resolv/NetBpfLoad-S.flag", F_OK);
+ bool mainlineNetBpfLoad = isAtLeastT || mainlineBpfCapableResolve;
+ DO_EXPECT(isAtLeastS && mainlineNetBpfLoad, 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);
for (const auto& file : mustExist) {
EXPECT_EQ(0, access(file.c_str(), R_OK)) << file << " does not exist";
diff --git a/clatd/ipv4.c b/clatd/ipv4.c
index 2be02e3..81bf87b 100644
--- a/clatd/ipv4.c
+++ b/clatd/ipv4.c
@@ -85,6 +85,11 @@
return 0;
}
+ if ((header->daddr & 0xf0000000) == 0xe0000000) {
+ logmsg_dbg(ANDROID_LOG_INFO, "ip_packet/daddr is multicast: %x", header->daddr);
+ return 0;
+ }
+
/* rfc6145 - If any IPv4 options are present in the IPv4 packet, they MUST be
* ignored and the packet translated normally; there is no attempt to
* translate the options.
diff --git a/common/flags.aconfig b/common/flags.aconfig
index 60a827b..51b4fc0 100644
--- a/common/flags.aconfig
+++ b/common/flags.aconfig
@@ -165,3 +165,12 @@
bug: "372936361"
is_fixed_read_only: true
}
+
+flag {
+ name: "restrict_local_network"
+ is_exported: true
+ namespace: "android_core_networking"
+ description: "Flag for controlling access to the local network behind a new runtime permission. Requires ConnectivityCompatChanges.RESTRICT_LOCAL_NETWORK to enable feature."
+ bug: "388774939"
+ is_fixed_read_only: true
+}
diff --git a/common/src/com/android/net/module/util/bpf/LocalNetAccessKey.java b/common/src/com/android/net/module/util/bpf/LocalNetAccessKey.java
new file mode 100644
index 0000000..95265b9
--- /dev/null
+++ b/common/src/com/android/net/module/util/bpf/LocalNetAccessKey.java
@@ -0,0 +1,72 @@
+/*
+ * 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.net.module.util.bpf;
+
+import com.android.net.module.util.InetAddressUtils;
+import com.android.net.module.util.Struct;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+
+public class LocalNetAccessKey extends Struct {
+
+ @Field(order = 0, type = Type.U32)
+ public final long lpmBitlen;
+ @Field(order = 1, type = Type.U32)
+ public final long ifIndex;
+ @Field(order = 2, type = Type.Ipv6Address)
+ public final Inet6Address remoteAddress;
+ @Field(order = 3, type = Type.U16)
+ public final int protocol;
+ @Field(order = 4, type = Type.UBE16)
+ public final int remotePort;
+
+ public LocalNetAccessKey(long lpmBitlen, long ifIndex, InetAddress remoteAddress, int protocol,
+ int remotePort) {
+ this.lpmBitlen = lpmBitlen;
+ this.ifIndex = ifIndex;
+ this.protocol = protocol;
+ this.remotePort = remotePort;
+
+ if (remoteAddress instanceof Inet4Address) {
+ this.remoteAddress = InetAddressUtils.v4MappedV6Address((Inet4Address) remoteAddress);
+ } else {
+ this.remoteAddress = (Inet6Address) remoteAddress;
+ }
+ }
+
+ public LocalNetAccessKey(long lpmBitlen, long ifIndex, Inet6Address remoteAddress, int protocol,
+ int remotePort) {
+ this.lpmBitlen = lpmBitlen;
+ this.ifIndex = ifIndex;
+ this.remoteAddress = remoteAddress;
+ this.protocol = protocol;
+ this.remotePort = remotePort;
+ }
+
+ @Override
+ public String toString() {
+ return "LocalNetAccessKey{"
+ + "lpmBitlen=" + lpmBitlen
+ + ", ifIndex=" + ifIndex
+ + ", remoteAddress=" + remoteAddress
+ + ", protocol=" + protocol
+ + ", remotePort=" + remotePort
+ + "}";
+ }
+}
diff --git a/framework/Android.bp b/framework/Android.bp
index a1c6a15..ab3af9a 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -160,7 +160,9 @@
java_defaults {
name: "CronetJavaDefaults",
srcs: [":httpclient_api_sources"],
- static_libs: ["com.android.net.http.flags-aconfig-java"],
+ static_libs: [
+ "com.android.net.http.flags-aconfig-java",
+ ],
libs: [
"androidx.annotation_annotation",
],
@@ -293,7 +295,6 @@
":framework-connectivity-t-pre-jarjar{.jar}",
":framework-connectivity.stubs.module_lib{.jar}",
":framework-connectivity-t.stubs.module_lib{.jar}",
- ":framework-connectivity-module-api-stubs-including-flagged{.jar}",
"jarjar-excludes.txt",
],
tools: [
@@ -306,7 +307,6 @@
"--prefix android.net.connectivity " +
"--apistubs $(location :framework-connectivity.stubs.module_lib{.jar}) " +
"--apistubs $(location :framework-connectivity-t.stubs.module_lib{.jar}) " +
- "--apistubs $(location :framework-connectivity-module-api-stubs-including-flagged{.jar}) " +
// Make a ":"-separated list. There will be an extra ":" but empty items are ignored.
"--unsupportedapi $$(printf ':%s' $(locations :connectivity-hiddenapi-files)) " +
"--excludes $(location jarjar-excludes.txt) " +
@@ -318,35 +318,6 @@
],
}
-droidstubs {
- name: "framework-connectivity-module-api-stubs-including-flagged-droidstubs",
- srcs: [
- ":framework-connectivity-sources",
- ":framework-connectivity-tiramisu-updatable-sources",
- ":framework-networksecurity-sources",
- ":framework-nearby-java-sources",
- ":framework-thread-sources",
- ],
- flags: [
- "--show-for-stub-purposes-annotation android.annotation.SystemApi" +
- "\\(client=android.annotation.SystemApi.Client.PRIVILEGED_APPS\\)",
- "--show-for-stub-purposes-annotation android.annotation.SystemApi" +
- "\\(client=android.annotation.SystemApi.Client.MODULE_LIBRARIES\\)",
- ],
- aidl: {
- include_dirs: [
- "packages/modules/Connectivity/framework/aidl-export",
- "packages/modules/Connectivity/Tethering/common/TetheringLib/src",
- "frameworks/native/aidl/binder", // For PersistableBundle.aidl
- ],
- },
-}
-
-java_library {
- name: "framework-connectivity-module-api-stubs-including-flagged",
- srcs: [":framework-connectivity-module-api-stubs-including-flagged-droidstubs"],
-}
-
// Library providing limited APIs within the connectivity module, so that R+ components like
// Tethering have a controlled way to depend on newer components like framework-connectivity that
// are not loaded on R.
diff --git a/framework/jni/android_net_NetworkUtils.cpp b/framework/jni/android_net_NetworkUtils.cpp
index 3779a00..7404f32 100644
--- a/framework/jni/android_net_NetworkUtils.cpp
+++ b/framework/jni/android_net_NetworkUtils.cpp
@@ -23,9 +23,9 @@
#include <netinet/in.h>
#include <string.h>
+#include <DnsProxydProtocol.h> // NETID_USE_LOCAL_NAMESERVERS
#include <bpf/BpfClassic.h>
#include <bpf/KernelUtils.h>
-#include <DnsProxydProtocol.h> // NETID_USE_LOCAL_NAMESERVERS
#include <nativehelper/JNIPlatformHelp.h>
#include <nativehelper/ScopedPrimitiveArray.h>
#include <utils/Log.h>
@@ -259,6 +259,21 @@
return bpf::isX86();
}
+static jlong android_net_utils_getSocketCookie(JNIEnv *env, jclass clazz,
+ jobject javaFd) {
+ int sock = AFileDescriptor_getFd(env, javaFd);
+ uint64_t cookie = 0;
+ socklen_t cookie_len = sizeof(cookie);
+ if (getsockopt(sock, SOL_SOCKET, SO_COOKIE, &cookie, &cookie_len)) {
+ // Failure is almost certainly either EBADF or ENOTSOCK
+ jniThrowErrnoException(env, "getSocketCookie", errno);
+ } else if (cookie_len != sizeof(cookie)) {
+ // This probably cannot actually happen, but...
+ jniThrowErrnoException(env, "getSocketCookie", 523); // EBADCOOKIE
+ }
+ return static_cast<jlong>(cookie);
+}
+
// ----------------------------------------------------------------------------
/*
@@ -283,6 +298,7 @@
(void*) android_net_utils_setsockoptBytes},
{ "isKernel64Bit", "()Z", (void*) android_net_utils_isKernel64Bit },
{ "isKernelX86", "()Z", (void*) android_net_utils_isKernelX86 },
+ { "getSocketCookie", "(Ljava/io/FileDescriptor;)J", (void*) android_net_utils_getSocketCookie },
};
// clang-format on
diff --git a/framework/src/android/net/BpfNetMapsConstants.java b/framework/src/android/net/BpfNetMapsConstants.java
index f3773de..f1a6f00 100644
--- a/framework/src/android/net/BpfNetMapsConstants.java
+++ b/framework/src/android/net/BpfNetMapsConstants.java
@@ -60,6 +60,11 @@
"/sys/fs/bpf/netd_shared/map_netd_data_saver_enabled_map";
public static final String INGRESS_DISCARD_MAP_PATH =
"/sys/fs/bpf/netd_shared/map_netd_ingress_discard_map";
+ public static final String LOCAL_NET_ACCESS_MAP_PATH =
+ "/sys/fs/bpf/netd_shared/map_netd_local_net_access_map";
+ public static final String LOCAL_NET_BLOCKED_UID_MAP_PATH =
+ "/sys/fs/bpf/netd_shared/map_netd_local_net_blocked_uid_map";
+
public static final Struct.S32 UID_RULES_CONFIGURATION_KEY = new Struct.S32(0);
public static final Struct.S32 CURRENT_STATS_MAP_CONFIGURATION_KEY = new Struct.S32(1);
public static final Struct.S32 DATA_SAVER_ENABLED_KEY = new Struct.S32(0);
diff --git a/framework/src/android/net/L2capNetworkSpecifier.java b/framework/src/android/net/L2capNetworkSpecifier.java
index cfc9ed9..93f9352 100644
--- a/framework/src/android/net/L2capNetworkSpecifier.java
+++ b/framework/src/android/net/L2capNetworkSpecifier.java
@@ -170,6 +170,51 @@
return mPsm;
}
+ /**
+ * Checks whether the given L2capNetworkSpecifier is valid as part of a server network
+ * reservation request.
+ *
+ * @hide
+ */
+ public boolean isValidServerReservationSpecifier() {
+ // The ROLE_SERVER offer can be satisfied by a ROLE_ANY request.
+ if (mRole != ROLE_SERVER) return false;
+
+ // HEADER_COMPRESSION_ANY is never valid in a request.
+ if (mHeaderCompression == HEADER_COMPRESSION_ANY) return false;
+
+ // Remote address must be null for ROLE_SERVER requests.
+ if (mRemoteAddress != null) return false;
+
+ // reservation must allocate a PSM, so only PSM_ANY can be passed.
+ if (mPsm != PSM_ANY) return false;
+
+ return true;
+ }
+
+ /**
+ * Checks whether the given L2capNetworkSpecifier is valid as part of a client network request.
+ *
+ * @hide
+ */
+ public boolean isValidClientRequestSpecifier() {
+ // The ROLE_CLIENT offer can be satisfied by a ROLE_ANY request.
+ if (mRole != ROLE_CLIENT) return false;
+
+ // HEADER_COMPRESSION_ANY is never valid in a request.
+ if (mHeaderCompression == HEADER_COMPRESSION_ANY) return false;
+
+ // Remote address must not be null for ROLE_CLIENT requests.
+ if (mRemoteAddress == null) return false;
+
+ // Client network requests require a PSM to be specified.
+ // Ensure the PSM is within the valid range of dynamic BLE L2CAP values.
+ if (mPsm < 0x80) return false;
+ if (mPsm > 0xFF) return false;
+
+ return true;
+ }
+
/** A builder class for L2capNetworkSpecifier. */
public static final class Builder {
@Role
diff --git a/framework/src/android/net/NetworkAgentConfig.java b/framework/src/android/net/NetworkAgentConfig.java
index deaa734..da12a0a 100644
--- a/framework/src/android/net/NetworkAgentConfig.java
+++ b/framework/src/android/net/NetworkAgentConfig.java
@@ -272,27 +272,6 @@
return mVpnRequiresValidation;
}
- /**
- * Whether the native network creation should be skipped.
- *
- * If set, the native network and routes should be maintained by the caller.
- *
- * @hide
- */
- private boolean mSkipNativeNetworkCreation = false;
-
-
- /**
- * @return Whether the native network creation should be skipped.
- * @hide
- */
- // TODO: Expose API when ready.
- // @FlaggedApi(Flags.FLAG_TETHERING_NETWORK_AGENT)
- // @SystemApi(client = MODULE_LIBRARIES) when ready.
- public boolean shouldSkipNativeNetworkCreation() {
- return mSkipNativeNetworkCreation;
- }
-
/** @hide */
public NetworkAgentConfig() {
}
@@ -314,7 +293,6 @@
mLegacyExtraInfo = nac.mLegacyExtraInfo;
excludeLocalRouteVpn = nac.excludeLocalRouteVpn;
mVpnRequiresValidation = nac.mVpnRequiresValidation;
- mSkipNativeNetworkCreation = nac.mSkipNativeNetworkCreation;
}
}
@@ -506,26 +484,6 @@
}
/**
- * Sets the native network creation should be skipped.
- *
- * @return this builder, to facilitate chaining.
- * @hide
- */
- @NonNull
- // TODO: Expose API when ready.
- // @FlaggedApi(Flags.FLAG_TETHERING_NETWORK_AGENT)
- // @SystemApi(client = MODULE_LIBRARIES) when ready.
- public Builder setSkipNativeNetworkCreation(boolean skipNativeNetworkCreation) {
- if (!SdkLevel.isAtLeastV()) {
- // Local agents are supported starting on U on TVs and on V on everything else.
- // Thus, only support this flag on V+.
- throw new UnsupportedOperationException("Method is not supported");
- }
- mConfig.mSkipNativeNetworkCreation = skipNativeNetworkCreation;
- return this;
- }
-
- /**
* Returns the constructed {@link NetworkAgentConfig} object.
*/
@NonNull
@@ -552,8 +510,7 @@
&& Objects.equals(legacySubTypeName, that.legacySubTypeName)
&& Objects.equals(mLegacyExtraInfo, that.mLegacyExtraInfo)
&& excludeLocalRouteVpn == that.excludeLocalRouteVpn
- && mVpnRequiresValidation == that.mVpnRequiresValidation
- && mSkipNativeNetworkCreation == that.mSkipNativeNetworkCreation;
+ && mVpnRequiresValidation == that.mVpnRequiresValidation;
}
@Override
@@ -561,8 +518,7 @@
return Objects.hash(allowBypass, explicitlySelected, acceptUnvalidated,
acceptPartialConnectivity, provisioningNotificationDisabled, subscriberId,
skip464xlat, legacyType, legacySubType, legacyTypeName, legacySubTypeName,
- mLegacyExtraInfo, excludeLocalRouteVpn, mVpnRequiresValidation,
- mSkipNativeNetworkCreation);
+ mLegacyExtraInfo, excludeLocalRouteVpn, mVpnRequiresValidation);
}
@Override
@@ -583,7 +539,6 @@
+ ", legacyExtraInfo = '" + mLegacyExtraInfo + '\''
+ ", excludeLocalRouteVpn = '" + excludeLocalRouteVpn + '\''
+ ", vpnRequiresValidation = '" + mVpnRequiresValidation + '\''
- + ", skipNativeNetworkCreation = '" + mSkipNativeNetworkCreation + '\''
+ "}";
}
@@ -608,35 +563,33 @@
out.writeString(mLegacyExtraInfo);
out.writeInt(excludeLocalRouteVpn ? 1 : 0);
out.writeInt(mVpnRequiresValidation ? 1 : 0);
- out.writeInt(mSkipNativeNetworkCreation ? 1 : 0);
}
public static final @NonNull Creator<NetworkAgentConfig> CREATOR =
new Creator<NetworkAgentConfig>() {
- @Override
- public NetworkAgentConfig createFromParcel(Parcel in) {
- NetworkAgentConfig networkAgentConfig = new NetworkAgentConfig();
- networkAgentConfig.allowBypass = in.readInt() != 0;
- networkAgentConfig.explicitlySelected = in.readInt() != 0;
- networkAgentConfig.acceptUnvalidated = in.readInt() != 0;
- networkAgentConfig.acceptPartialConnectivity = in.readInt() != 0;
- networkAgentConfig.subscriberId = in.readString();
- networkAgentConfig.provisioningNotificationDisabled = in.readInt() != 0;
- networkAgentConfig.skip464xlat = in.readInt() != 0;
- networkAgentConfig.legacyType = in.readInt();
- networkAgentConfig.legacyTypeName = in.readString();
- networkAgentConfig.legacySubType = in.readInt();
- networkAgentConfig.legacySubTypeName = in.readString();
- networkAgentConfig.mLegacyExtraInfo = in.readString();
- networkAgentConfig.excludeLocalRouteVpn = in.readInt() != 0;
- networkAgentConfig.mVpnRequiresValidation = in.readInt() != 0;
- networkAgentConfig.mSkipNativeNetworkCreation = in.readInt() != 0;
- return networkAgentConfig;
- }
+ @Override
+ public NetworkAgentConfig createFromParcel(Parcel in) {
+ NetworkAgentConfig networkAgentConfig = new NetworkAgentConfig();
+ networkAgentConfig.allowBypass = in.readInt() != 0;
+ networkAgentConfig.explicitlySelected = in.readInt() != 0;
+ networkAgentConfig.acceptUnvalidated = in.readInt() != 0;
+ networkAgentConfig.acceptPartialConnectivity = in.readInt() != 0;
+ networkAgentConfig.subscriberId = in.readString();
+ networkAgentConfig.provisioningNotificationDisabled = in.readInt() != 0;
+ networkAgentConfig.skip464xlat = in.readInt() != 0;
+ networkAgentConfig.legacyType = in.readInt();
+ networkAgentConfig.legacyTypeName = in.readString();
+ networkAgentConfig.legacySubType = in.readInt();
+ networkAgentConfig.legacySubTypeName = in.readString();
+ networkAgentConfig.mLegacyExtraInfo = in.readString();
+ networkAgentConfig.excludeLocalRouteVpn = in.readInt() != 0;
+ networkAgentConfig.mVpnRequiresValidation = in.readInt() != 0;
+ return networkAgentConfig;
+ }
- @Override
- public NetworkAgentConfig[] newArray(int size) {
- return new NetworkAgentConfig[size];
- }
- };
+ @Override
+ public NetworkAgentConfig[] newArray(int size) {
+ return new NetworkAgentConfig[size];
+ }
+ };
}
diff --git a/framework/src/android/net/NetworkUtils.java b/framework/src/android/net/NetworkUtils.java
index 18feb84..6b2eb08 100644
--- a/framework/src/android/net/NetworkUtils.java
+++ b/framework/src/android/net/NetworkUtils.java
@@ -443,4 +443,13 @@
/** Returns whether the Linux Kernel is x86 */
public static native boolean isKernelX86();
+
+ /**
+ * Returns socket cookie.
+ *
+ * @param fd The socket file descriptor
+ * @return The socket cookie.
+ * @throws ErrnoException if retrieving the socket cookie fails.
+ */
+ public static native long getSocketCookie(FileDescriptor fd) throws ErrnoException;
}
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..2261c69 100644
--- a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
+++ b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
@@ -100,9 +100,9 @@
public static final long ENABLE_MATCH_LOCAL_NETWORK = 319212206L;
/**
- * On Android {@link android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM} or higher releases,
- * network access from apps targeting Android 36 or higher that do not have the
- * {@link android.Manifest.permission#INTERNET} permission is considered blocked.
+ * On Android versions starting from 37, network access from apps targeting
+ * Android 37 or higher, that do not have the {@link android.Manifest.permission#INTERNET}
+ * permission, is considered blocked.
* This results in API behaviors change for apps without
* {@link android.Manifest.permission#INTERNET} permission.
* {@link android.net.NetworkInfo} returned from {@link android.net.ConnectivityManager} APIs
@@ -115,10 +115,12 @@
* network access from apps without {@link android.Manifest.permission#INTERNET} permission is
* considered not blocked even though apps cannot access any networks.
*
+ * TODO: b/400903101 - Update the target SDK version once it's finalized.
+ *
* @hide
*/
@ChangeId
- @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @EnabledAfter(targetSdkVersion = 36)
public static final long NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION = 333340911L;
/**
@@ -143,7 +145,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/nearby/framework/java/android/nearby/NearbyManager.java b/nearby/framework/java/android/nearby/NearbyManager.java
index e78f999..9d7d144 100644
--- a/nearby/framework/java/android/nearby/NearbyManager.java
+++ b/nearby/framework/java/android/nearby/NearbyManager.java
@@ -46,6 +46,7 @@
import java.util.WeakHashMap;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
+import java.util.stream.Collectors;
/**
* This class provides a way to perform Nearby related operations such as scanning, broadcasting
@@ -503,7 +504,7 @@
PoweredOffFindingEphemeralId ephemeralId = new PoweredOffFindingEphemeralId();
ephemeralId.bytes = eid;
return ephemeralId;
- }).toList();
+ }).collect(Collectors.toUnmodifiableList());
try {
mService.setPoweredOffFindingEphemeralIds(ephemeralIdList);
} catch (RemoteException e) {
diff --git a/networksecurity/TEST_MAPPING b/networksecurity/TEST_MAPPING
index f9238c3..448ee84 100644
--- a/networksecurity/TEST_MAPPING
+++ b/networksecurity/TEST_MAPPING
@@ -10,6 +10,9 @@
"name": "NetSecConfigCertificateTransparencySctLogListTestCases"
},
{
+ "name": "NetSecConfigCertificateTransparencySctNoLogListTestCases"
+ },
+ {
"name": "NetworkSecurityUnitTests"
}
]
diff --git a/networksecurity/service/Android.bp b/networksecurity/service/Android.bp
index d7aacdb..3c964e5 100644
--- a/networksecurity/service/Android.bp
+++ b/networksecurity/service/Android.bp
@@ -32,6 +32,7 @@
"framework-connectivity-pre-jarjar",
"service-connectivity-pre-jarjar",
"framework-statsd.stubs.module_lib",
+ "ServiceConnectivityResources",
],
static_libs: [
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
index ce14fc6..fb42c03 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
@@ -16,32 +16,26 @@
package com.android.server.net.ct;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_HTTP_ERROR;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_NOT_FOUND;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_VERSION_ALREADY_EXISTS;
-
import android.annotation.RequiresApi;
import android.app.DownloadManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
-import android.content.IntentFilter;
import android.net.Uri;
import android.os.Build;
-import android.provider.DeviceConfig;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
+import com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState;
import com.android.server.net.ct.DownloadHelper.DownloadStatus;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
-import java.security.InvalidKeyException;
import java.util.ArrayList;
import java.util.List;
+import java.util.Optional;
/** Helper class to download certificate transparency log files. */
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@@ -57,8 +51,6 @@
private final List<CompatibilityVersion> mCompatVersions = new ArrayList<>();
- private boolean started = false;
-
CertificateTransparencyDownloader(
Context context,
DataStore dataStore,
@@ -76,33 +68,8 @@
mCompatVersions.add(compatVersion);
}
- void start() {
- if (started) {
- return;
- }
- mContext.registerReceiver(
- this,
- new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
- Context.RECEIVER_EXPORTED);
- mDataStore.load();
- started = true;
-
- if (Config.DEBUG) {
- Log.d(TAG, "CertificateTransparencyDownloader started.");
- }
- }
-
- void stop() {
- if (!started) {
- return;
- }
- mContext.unregisterReceiver(this);
- mDataStore.delete();
- started = false;
-
- if (Config.DEBUG) {
- Log.d(TAG, "CertificateTransparencyDownloader stopped.");
- }
+ void clearCompatibilityVersions() {
+ mCompatVersions.clear();
}
long startPublicKeyDownload() {
@@ -231,106 +198,40 @@
return;
}
- boolean success = false;
- int failureReason = -1;
+ LogListUpdateStatus updateStatus = mSignatureVerifier.verify(contentUri, metadataUri);
- try {
- success = mSignatureVerifier.verify(contentUri, metadataUri);
- } catch (MissingPublicKeyException e) {
- if (updateFailureCount()) {
- failureReason = CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_NOT_FOUND;
- }
- } catch (InvalidKeyException e) {
- if (updateFailureCount()) {
- failureReason = CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION;
- }
- } catch (IOException | GeneralSecurityException e) {
- Log.e(TAG, "Could not verify new log list", e);
- }
-
- if (!success) {
+ if (!updateStatus.isSignatureVerified()) {
Log.w(TAG, "Log list did not pass verification");
- // Avoid logging failure twice
- if (failureReason == -1 && updateFailureCount()) {
- failureReason = CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION;
- }
+ mLogger.logCTLogListUpdateStateChangedEvent(updateStatus);
- if (failureReason != -1) {
- mLogger.logCTLogListUpdateFailedEvent(
- failureReason,
- mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0));
- }
return;
}
try (InputStream inputStream = mContext.getContentResolver().openInputStream(contentUri)) {
- success = compatVersion.install(inputStream);
+ updateStatus = compatVersion.install(inputStream, updateStatus.toBuilder());
} catch (IOException e) {
Log.e(TAG, "Could not install new content", e);
return;
}
- if (success) {
- // Reset the number of consecutive log list failure updates back to zero.
- mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* value= */ 0);
- mDataStore.store();
- } else {
- if (updateFailureCount()) {
- mLogger.logCTLogListUpdateFailedEvent(
- CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_VERSION_ALREADY_EXISTS,
- mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0));
- }
- }
+ mLogger.logCTLogListUpdateStateChangedEvent(updateStatus);
}
private void handleDownloadFailed(DownloadStatus status) {
Log.e(TAG, "Download failed with " + status);
- if (updateFailureCount()) {
- int failureCount =
- mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0);
-
- if (status.isHttpError()) {
- mLogger.logCTLogListUpdateFailedEvent(
- CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_HTTP_ERROR,
- failureCount,
- status.reason());
- } else {
- // TODO(b/384935059): handle blocked domain logging
- mLogger.logCTLogListUpdateFailedEventWithDownloadStatus(
- status.reason(), failureCount);
- }
+ LogListUpdateStatus.Builder updateStatusBuilder = LogListUpdateStatus.builder();
+ if (status.isHttpError()) {
+ updateStatusBuilder
+ .setState(CTLogListUpdateState.HTTP_ERROR)
+ .setHttpErrorStatusCode(status.reason());
+ } else {
+ // TODO(b/384935059): handle blocked domain logging
+ updateStatusBuilder.setDownloadStatus(Optional.of(status.reason()));
}
- }
- /**
- * Updates the data store with the current number of consecutive log list update failures.
- *
- * @return whether the failure count exceeds the threshold and should be logged.
- */
- private boolean updateFailureCount() {
- int failure_count =
- mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0);
- int new_failure_count = failure_count + 1;
-
- mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, new_failure_count);
- mDataStore.store();
-
- int threshold = DeviceConfig.getInt(
- Config.NAMESPACE_NETWORK_SECURITY,
- Config.FLAG_LOG_FAILURE_THRESHOLD,
- Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD);
-
- boolean shouldReport = new_failure_count >= threshold;
- if (shouldReport) {
- Log.d(TAG, "Log list update failure count exceeds threshold: " + new_failure_count);
- }
- return shouldReport;
+ mLogger.logCTLogListUpdateStateChangedEvent(updateStatusBuilder.build());
}
private long download(String url) {
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
index a8acc60..f1b9a4f 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
@@ -17,12 +17,12 @@
import android.annotation.RequiresApi;
import android.app.AlarmManager;
+import android.app.DownloadManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
-import android.content.pm.PackageManager;
import android.os.Build;
import android.os.ConfigUpdate;
import android.os.SystemClock;
@@ -33,28 +33,31 @@
public class CertificateTransparencyJob extends BroadcastReceiver {
private static final String TAG = "CertificateTransparencyJob";
- private static final String UPDATE_CONFIG_PERMISSION = "android.permission.UPDATE_CONFIG";
private final Context mContext;
- private final CompatibilityVersion mCompatVersion;
+ private final DataStore mDataStore;
private final CertificateTransparencyDownloader mCertificateTransparencyDownloader;
+ private final CompatibilityVersion mCompatVersion;
+ private final SignatureVerifier mSignatureVerifier;
private final AlarmManager mAlarmManager;
private final PendingIntent mPendingIntent;
+ private boolean mScheduled = false;
private boolean mDependenciesReady = false;
/** Creates a new {@link CertificateTransparencyJob} object. */
public CertificateTransparencyJob(
- Context context, CertificateTransparencyDownloader certificateTransparencyDownloader) {
+ Context context,
+ DataStore dataStore,
+ CertificateTransparencyDownloader certificateTransparencyDownloader,
+ CompatibilityVersion compatVersion,
+ SignatureVerifier signatureVerifier) {
mContext = context;
- mCompatVersion =
- new CompatibilityVersion(
- Config.COMPATIBILITY_VERSION,
- Config.URL_SIGNATURE,
- Config.URL_LOG_LIST,
- Config.CT_ROOT_DIRECTORY_PATH);
+ mDataStore = dataStore;
mCertificateTransparencyDownloader = certificateTransparencyDownloader;
- mCertificateTransparencyDownloader.addCompatibilityVersion(mCompatVersion);
+ mCompatVersion = compatVersion;
+ mSignatureVerifier = signatureVerifier;
+
mAlarmManager = context.getSystemService(AlarmManager.class);
mPendingIntent =
PendingIntent.getBroadcast(
@@ -65,15 +68,19 @@
}
void schedule() {
- mContext.registerReceiver(
- this,
- new IntentFilter(ConfigUpdate.ACTION_UPDATE_CT_LOGS),
- Context.RECEIVER_EXPORTED);
- mAlarmManager.setInexactRepeating(
- AlarmManager.ELAPSED_REALTIME,
- SystemClock.elapsedRealtime(), // schedule first job at earliest convenient time.
- AlarmManager.INTERVAL_DAY,
- mPendingIntent);
+ if (!mScheduled) {
+ mContext.registerReceiver(
+ this,
+ new IntentFilter(ConfigUpdate.ACTION_UPDATE_CT_LOGS),
+ Context.RECEIVER_EXPORTED);
+ mAlarmManager.setInexactRepeating(
+ AlarmManager.ELAPSED_REALTIME,
+ SystemClock
+ .elapsedRealtime(), // schedule first job at earliest convenient time.
+ AlarmManager.INTERVAL_DAY,
+ mPendingIntent);
+ }
+ mScheduled = true;
if (Config.DEBUG) {
Log.d(TAG, "CertificateTransparencyJob scheduled.");
@@ -81,12 +88,19 @@
}
void cancel() {
- mContext.unregisterReceiver(this);
- mAlarmManager.cancel(mPendingIntent);
- mCertificateTransparencyDownloader.stop();
- mCompatVersion.delete();
+ if (mScheduled) {
+ mContext.unregisterReceiver(this);
+ mAlarmManager.cancel(mPendingIntent);
+ }
+ mScheduled = false;
+
+ if (mDependenciesReady) {
+ stopDependencies();
+ }
mDependenciesReady = false;
+ mCompatVersion.delete();
+
if (Config.DEBUG) {
Log.d(TAG, "CertificateTransparencyJob canceled.");
}
@@ -98,16 +112,11 @@
Log.w(TAG, "Received unexpected broadcast with action " + intent);
return;
}
- if (context.checkCallingOrSelfPermission(UPDATE_CONFIG_PERMISSION)
- != PackageManager.PERMISSION_GRANTED) {
- Log.e(TAG, "Caller does not have UPDATE_CONFIG permission.");
- return;
- }
if (Config.DEBUG) {
Log.d(TAG, "Starting CT daily job.");
}
if (!mDependenciesReady) {
- mCertificateTransparencyDownloader.start();
+ startDependencies();
mDependenciesReady = true;
}
@@ -117,4 +126,29 @@
Log.d(TAG, "Public key download started successfully.");
}
}
+
+ private void startDependencies() {
+ mDataStore.load();
+ mCertificateTransparencyDownloader.addCompatibilityVersion(mCompatVersion);
+ mSignatureVerifier.loadAllowedKeys();
+ mContext.registerReceiver(
+ mCertificateTransparencyDownloader,
+ new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
+ Context.RECEIVER_EXPORTED);
+
+ if (Config.DEBUG) {
+ Log.d(TAG, "CertificateTransparencyJob dependencies ready.");
+ }
+ }
+
+ private void stopDependencies() {
+ mContext.unregisterReceiver(mCertificateTransparencyDownloader);
+ mSignatureVerifier.clearAllowedKeys();
+ mCertificateTransparencyDownloader.clearCompatibilityVersions();
+ mDataStore.delete();
+
+ if (Config.DEBUG) {
+ Log.d(TAG, "CertificateTransparencyJob dependencies stopped.");
+ }
+ }
}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java
index 913c472..2a37d8f 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java
@@ -20,29 +20,27 @@
public interface CertificateTransparencyLogger {
/**
- * Logs a CTLogListUpdateFailed event to statsd, when failure is provided by DownloadManager.
+ * Logs a CTLogListUpdateStateChanged event to statsd.
*
- * @param downloadStatus DownloadManager failure status why the log list wasn't updated
- * @param failureCount number of consecutive log list update failures
+ * @param updateStatus status object containing details from this update event (e.g. log list
+ * signature, log list timestamp, failure reason if applicable)
*/
- void logCTLogListUpdateFailedEventWithDownloadStatus(int downloadStatus, int failureCount);
+ void logCTLogListUpdateStateChangedEvent(LogListUpdateStatus updateStatus);
/**
- * Logs a CTLogListUpdateFailed event to statsd, when no HTTP error status code is present.
+ * Intermediate enum for use with CertificateTransparencyStatsLog.
*
- * @param failureReason reason why the log list wasn't updated
- * @param failureCount number of consecutive log list update failures
+ * This enum primarily exists to avoid 100+ char line alert fatigue.
*/
- void logCTLogListUpdateFailedEvent(int failureReason, int failureCount);
-
- /**
- * Logs a CTLogListUpdateFailed event to statsd, when an HTTP error status code is provided.
- *
- * @param failureReason reason why the log list wasn't updated (e.g. DownloadManager failures)
- * @param failureCount number of consecutive log list update failures
- * @param httpErrorStatusCode if relevant, the HTTP error status code from DownloadManager
- */
- void logCTLogListUpdateFailedEvent(
- int failureReason, int failureCount, int httpErrorStatusCode);
-
+ enum CTLogListUpdateState {
+ UNKNOWN_STATE,
+ HTTP_ERROR,
+ LOG_LIST_INVALID,
+ PUBLIC_KEY_NOT_FOUND,
+ SIGNATURE_INVALID,
+ SIGNATURE_NOT_FOUND,
+ SIGNATURE_VERIFICATION_FAILED,
+ SUCCESS,
+ VERSION_ALREADY_EXISTS
+ }
}
\ No newline at end of file
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java
index b97a885..f617523 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java
@@ -16,40 +16,92 @@
package com.android.server.net.ct;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_DEVICE_OFFLINE;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_DOWNLOAD_CANNOT_RESUME;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_HTTP_ERROR;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_NO_DISK_SPACE;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_TOO_MANY_REDIRECTS;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_UNKNOWN;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__PENDING_WAITING_FOR_WIFI;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_DEVICE_OFFLINE;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_DOWNLOAD_CANNOT_RESUME;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_HTTP_ERROR;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_LOG_LIST_INVALID;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_NO_DISK_SPACE;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_PUBLIC_KEY_NOT_FOUND;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_INVALID;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_NOT_FOUND;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_VERIFICATION;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_TOO_MANY_REDIRECTS;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_UNKNOWN;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_VERSION_ALREADY_EXISTS;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__PENDING_WAITING_FOR_WIFI;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__SUCCESS;
import android.app.DownloadManager;
/** Implementation for logging to statsd for Certificate Transparency. */
class CertificateTransparencyLoggerImpl implements CertificateTransparencyLogger {
- @Override
- public void logCTLogListUpdateFailedEventWithDownloadStatus(
- int downloadStatus, int failureCount) {
- logCTLogListUpdateFailedEvent(downloadStatusToFailureReason(downloadStatus), failureCount);
+ private final DataStore mDataStore;
+
+ CertificateTransparencyLoggerImpl(DataStore dataStore) {
+ mDataStore = dataStore;
}
@Override
- public void logCTLogListUpdateFailedEvent(int failureReason, int failureCount) {
- logCTLogListUpdateFailedEvent(failureReason, failureCount, /* httpErrorStatusCode= */ 0);
- }
+ public void logCTLogListUpdateStateChangedEvent(LogListUpdateStatus updateStatus) {
+ if (updateStatus.isSuccessful()) {
+ resetFailureCount();
+ } else {
+ updateFailureCount();
+ }
- @Override
- public void logCTLogListUpdateFailedEvent(
- int failureReason, int failureCount, int httpErrorStatusCode) {
- CertificateTransparencyStatsLog.write(
- CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED,
- failureReason,
+ int updateState =
+ updateStatus
+ .downloadStatus()
+ .map(s -> downloadStatusToFailureReason(s))
+ .orElseGet(() -> localEnumToStatsLogEnum(updateStatus.state()));
+ int failureCount =
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0);
+
+ logCTLogListUpdateStateChangedEvent(
+ updateState,
failureCount,
- httpErrorStatusCode
- );
+ updateStatus.httpErrorStatusCode(),
+ updateStatus.signature(),
+ updateStatus.logListTimestamp());
+ }
+
+ private void logCTLogListUpdateStateChangedEvent(
+ int updateState,
+ int failureCount,
+ int httpErrorStatusCode,
+ String signature,
+ long logListTimestamp) {
+ CertificateTransparencyStatsLog.write(
+ CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED,
+ updateState,
+ failureCount,
+ httpErrorStatusCode,
+ signature,
+ logListTimestamp);
+ }
+
+ /**
+ * Resets the number of consecutive log list update failures in the data store back to zero.
+ */
+ private void resetFailureCount() {
+ mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* value= */ 0);
+ mDataStore.store();
+ }
+
+ /**
+ * Updates the data store with the current number of consecutive log list update failures.
+ */
+ private void updateFailureCount() {
+ int failure_count =
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0);
+ int new_failure_count = failure_count + 1;
+
+ mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, new_failure_count);
+ mDataStore.store();
}
/** Converts DownloadStatus reason into failure reason to log. */
@@ -57,21 +109,45 @@
switch (downloadStatusReason) {
case DownloadManager.PAUSED_WAITING_TO_RETRY:
case DownloadManager.PAUSED_WAITING_FOR_NETWORK:
- return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_DEVICE_OFFLINE;
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_DEVICE_OFFLINE;
case DownloadManager.ERROR_UNHANDLED_HTTP_CODE:
case DownloadManager.ERROR_HTTP_DATA_ERROR:
- return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_HTTP_ERROR;
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_HTTP_ERROR;
case DownloadManager.ERROR_TOO_MANY_REDIRECTS:
- return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_TOO_MANY_REDIRECTS;
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_TOO_MANY_REDIRECTS;
case DownloadManager.ERROR_CANNOT_RESUME:
- return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_DOWNLOAD_CANNOT_RESUME;
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_DOWNLOAD_CANNOT_RESUME;
case DownloadManager.ERROR_INSUFFICIENT_SPACE:
- return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_NO_DISK_SPACE;
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_NO_DISK_SPACE;
case DownloadManager.PAUSED_QUEUED_FOR_WIFI:
- return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__PENDING_WAITING_FOR_WIFI;
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__PENDING_WAITING_FOR_WIFI;
default:
- return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_UNKNOWN;
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_UNKNOWN;
}
}
+ /** Converts the local enum to the corresponding auto-generated one used by CTStatsLog. */
+ private int localEnumToStatsLogEnum(CTLogListUpdateState updateState) {
+ switch (updateState) {
+ case HTTP_ERROR:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_HTTP_ERROR;
+ case LOG_LIST_INVALID:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_LOG_LIST_INVALID;
+ case PUBLIC_KEY_NOT_FOUND:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_PUBLIC_KEY_NOT_FOUND;
+ case SIGNATURE_INVALID:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_INVALID;
+ case SIGNATURE_NOT_FOUND:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_NOT_FOUND;
+ case SIGNATURE_VERIFICATION_FAILED:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_VERIFICATION;
+ case SUCCESS:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__SUCCESS;
+ case VERSION_ALREADY_EXISTS:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_VERSION_ALREADY_EXISTS;
+ case UNKNOWN_STATE:
+ default:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_UNKNOWN;
+ }
+ }
}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
index ed98056..2e910b2 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
@@ -41,8 +41,6 @@
private final CertificateTransparencyJob mCertificateTransparencyJob;
- private boolean started = false;
-
/**
* @return true if the CertificateTransparency service is enabled.
*/
@@ -53,16 +51,24 @@
/** Creates a new {@link CertificateTransparencyService} object. */
public CertificateTransparencyService(Context context) {
DataStore dataStore = new DataStore(Config.PREFERENCES_FILE);
- DownloadHelper downloadHelper = new DownloadHelper(context);
+
SignatureVerifier signatureVerifier = new SignatureVerifier(context);
- CertificateTransparencyDownloader downloader =
- new CertificateTransparencyDownloader(
+ mCertificateTransparencyJob =
+ new CertificateTransparencyJob(
context,
dataStore,
- downloadHelper,
- signatureVerifier,
- new CertificateTransparencyLoggerImpl());
- mCertificateTransparencyJob = new CertificateTransparencyJob(context, downloader);
+ new CertificateTransparencyDownloader(
+ context,
+ dataStore,
+ new DownloadHelper(context),
+ signatureVerifier,
+ new CertificateTransparencyLoggerImpl(dataStore)),
+ new CompatibilityVersion(
+ Config.COMPATIBILITY_VERSION,
+ Config.URL_SIGNATURE,
+ Config.URL_LOG_LIST,
+ Config.CT_ROOT_DIRECTORY_PATH),
+ signatureVerifier);
}
/**
@@ -104,19 +110,13 @@
if (Config.DEBUG) {
Log.d(TAG, "CertificateTransparencyService start");
}
- if (!started) {
- mCertificateTransparencyJob.schedule();
- started = true;
- }
+ mCertificateTransparencyJob.schedule();
}
private void stopService() {
if (Config.DEBUG) {
Log.d(TAG, "CertificateTransparencyService stop");
}
- if (started) {
- mCertificateTransparencyJob.cancel();
- started = false;
- }
+ mCertificateTransparencyJob.cancel();
}
}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java b/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
index 9d60163..e8a6e64 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
@@ -23,6 +23,8 @@
import android.system.Os;
import android.util.Log;
+import com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState;
+
import org.json.JSONException;
import org.json.JSONObject;
@@ -69,22 +71,29 @@
* Installs a log list within this compatibility version directory.
*
* @param newContent an input stream providing the log list
+ * @param statusBuilder status obj builder containing details of the log list update process
* @return true if the log list was installed successfully, false otherwise.
* @throws IOException if the list cannot be saved in the CT directory.
*/
- boolean install(InputStream newContent) throws IOException {
+ LogListUpdateStatus install(
+ InputStream newContent, LogListUpdateStatus.Builder statusBuilder) throws IOException {
String content = new String(newContent.readAllBytes(), UTF_8);
try {
+ JSONObject contentJson = new JSONObject(content);
return install(
new ByteArrayInputStream(content.getBytes()),
- new JSONObject(content).getString("version"));
+ contentJson.getString("version"),
+ statusBuilder.setLogListTimestamp(contentJson.getLong("log_list_timestamp")));
} catch (JSONException e) {
Log.e(TAG, "invalid log list format", e);
- return false;
+
+ return statusBuilder.setState(CTLogListUpdateState.LOG_LIST_INVALID).build();
}
}
- private boolean install(InputStream newContent, String version) throws IOException {
+ LogListUpdateStatus install(
+ InputStream newContent, String version, LogListUpdateStatus.Builder statusBuilder)
+ throws IOException {
// To support atomically replacing the old configuration directory with the new
// there's a bunch of steps. We create a new directory with the logs and then do
// an atomic update of the current symlink to point to the new directory.
@@ -100,7 +109,7 @@
if (newLogsDir.getCanonicalPath().equals(mCurrentLogsDirSymlink.getCanonicalPath())) {
Log.i(TAG, newLogsDir + " already exists, skipping install.");
deleteOldLogDirectories();
- return false;
+ return statusBuilder.setState(CTLogListUpdateState.VERSION_ALREADY_EXISTS).build();
}
// If the symlink has not been updated then the previous installation failed and
// this is a re-attempt. Clean-up leftover files and try again.
@@ -134,7 +143,7 @@
// 7. Cleanup
Log.i(TAG, "New logs installed at " + newLogsDir);
deleteOldLogDirectories();
- return true;
+ return statusBuilder.setState(CTLogListUpdateState.SUCCESS).build();
}
String getCompatVersion() {
diff --git a/networksecurity/service/src/com/android/server/net/ct/Config.java b/networksecurity/service/src/com/android/server/net/ct/Config.java
index bc4efab..5fdba09 100644
--- a/networksecurity/service/src/com/android/server/net/ct/Config.java
+++ b/networksecurity/service/src/com/android/server/net/ct/Config.java
@@ -45,7 +45,6 @@
static final String FLAG_METADATA_URL = FLAGS_PREFIX + "metadata_url";
static final String FLAG_VERSION = FLAGS_PREFIX + "version";
static final String FLAG_PUBLIC_KEY = FLAGS_PREFIX + "public_key";
- static final String FLAG_LOG_FAILURE_THRESHOLD = FLAGS_PREFIX + "log_list_failure_threshold";
// properties
static final String VERSION = "version";
@@ -59,7 +58,4 @@
static final String URL_LOG_LIST = URL_PREFIX + "log_list.json";
static final String URL_SIGNATURE = URL_PREFIX + "log_list.sig";
static final String URL_PUBLIC_KEY = URL_PREFIX + "log_list.pub";
-
- // Threshold amounts
- static final int DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD = 10;
}
diff --git a/networksecurity/service/src/com/android/server/net/ct/DataStore.java b/networksecurity/service/src/com/android/server/net/ct/DataStore.java
index 8180316..1f99efa 100644
--- a/networksecurity/service/src/com/android/server/net/ct/DataStore.java
+++ b/networksecurity/service/src/com/android/server/net/ct/DataStore.java
@@ -44,8 +44,9 @@
}
try (InputStream in = new FileInputStream(mPropertyFile)) {
load(in);
- } catch (IOException e) {
+ } catch (IOException | IllegalArgumentException e) {
Log.e(TAG, "Error loading property store", e);
+ delete();
}
}
diff --git a/networksecurity/service/src/com/android/server/net/ct/LogListUpdateStatus.java b/networksecurity/service/src/com/android/server/net/ct/LogListUpdateStatus.java
new file mode 100644
index 0000000..3f9b762
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/LogListUpdateStatus.java
@@ -0,0 +1,89 @@
+/*
+ * 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.net.ct;
+
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.PUBLIC_KEY_NOT_FOUND;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_INVALID;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_NOT_FOUND;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_VERIFICATION_FAILED;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SUCCESS;
+
+import com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.Optional;
+
+/** Class to represent the signature verification status for Certificate Transparency. */
+@AutoValue
+public abstract class LogListUpdateStatus {
+
+ abstract CTLogListUpdateState state();
+
+ abstract String signature();
+
+ abstract long logListTimestamp();
+
+ abstract int httpErrorStatusCode();
+
+ abstract Optional<Integer> downloadStatus();
+
+ boolean isSignatureVerified() {
+ // Check that none of the signature verification failures have been set as the state
+ return state() != PUBLIC_KEY_NOT_FOUND
+ && state() != SIGNATURE_INVALID
+ && state() != SIGNATURE_NOT_FOUND
+ && state() != SIGNATURE_VERIFICATION_FAILED;
+ }
+
+ boolean hasSignature() {
+ return signature() != null && signature().length() > 0;
+ }
+
+ boolean isSuccessful() {
+ return state() == SUCCESS;
+ }
+
+ static LogListUpdateStatus getDefaultInstance() {
+ return builder().build();
+ }
+
+ @AutoValue.Builder
+ abstract static class Builder {
+ abstract Builder setState(CTLogListUpdateState updateState);
+
+ abstract Builder setSignature(String signature);
+
+ abstract Builder setLogListTimestamp(long timestamp);
+
+ abstract Builder setHttpErrorStatusCode(int httpStatusCode);
+
+ abstract Builder setDownloadStatus(Optional<Integer> downloadStatus);
+
+ abstract LogListUpdateStatus build();
+ }
+
+ abstract LogListUpdateStatus.Builder toBuilder();
+
+ static Builder builder() {
+ return new AutoValue_LogListUpdateStatus.Builder()
+ .setState(CTLogListUpdateState.UNKNOWN_STATE)
+ .setSignature("")
+ .setLogListTimestamp(0L)
+ .setHttpErrorStatusCode(0)
+ .setDownloadStatus(Optional.empty());
+ }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/MissingPublicKeyException.java b/networksecurity/service/src/com/android/server/net/ct/MissingPublicKeyException.java
deleted file mode 100644
index 80607f6..0000000
--- a/networksecurity/service/src/com/android/server/net/ct/MissingPublicKeyException.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * 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.net.ct;
-
-/**
- * An exception thrown when the public key is missing for CT signature verification.
- */
-public class MissingPublicKeyException extends Exception {
-
- public MissingPublicKeyException(String message) {
- super(message);
- }
-}
diff --git a/networksecurity/service/src/com/android/server/net/ct/PemReader.java b/networksecurity/service/src/com/android/server/net/ct/PemReader.java
new file mode 100644
index 0000000..56b3973
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/PemReader.java
@@ -0,0 +1,81 @@
+/*
+ * 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.ct;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.PublicKey;
+import java.security.spec.KeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Collection;
+
+/** Utility class to read keys in PEM format. */
+class PemReader {
+
+ private static final String BEGIN = "-----BEGIN";
+ private static final String END = "-----END";
+
+ /**
+ * Parse the provided input stream and return the list of keys from the stream.
+ *
+ * @param input the input stream
+ * @return the keys
+ */
+ public static Collection<PublicKey> readKeysFrom(InputStream input)
+ throws IOException, GeneralSecurityException {
+ KeyFactory instance = KeyFactory.getInstance("RSA");
+ Collection<PublicKey> keys = new ArrayList<>();
+
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(input))) {
+ String line = reader.readLine();
+ while (line != null) {
+ if (line.startsWith(BEGIN)) {
+ keys.add(instance.generatePublic(readNextKey(reader)));
+ } else {
+ throw new IOException("Unexpected line in the reader: " + line);
+ }
+ line = reader.readLine();
+ }
+ } catch (IllegalArgumentException e) {
+ throw new GeneralSecurityException("Invalid public key base64 encoding", e);
+ }
+
+ return keys;
+ }
+
+ private static KeySpec readNextKey(BufferedReader reader) throws IOException {
+ StringBuilder publicKeyBuilder = new StringBuilder();
+
+ String line = reader.readLine();
+ while (line != null) {
+ if (line.startsWith(END)) {
+ return new X509EncodedKeySpec(
+ Base64.getDecoder().decode(publicKeyBuilder.toString()));
+ } else {
+ publicKeyBuilder.append(line);
+ }
+ line = reader.readLine();
+ }
+
+ throw new IOException("Unexpected end of the reader");
+ }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java b/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
index 96488fc..87a4973 100644
--- a/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
+++ b/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
@@ -15,37 +15,66 @@
*/
package com.android.server.net.ct;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.PUBLIC_KEY_NOT_FOUND;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_INVALID;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_NOT_FOUND;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_VERIFICATION_FAILED;
+
import android.annotation.NonNull;
import android.annotation.RequiresApi;
import android.content.ContentResolver;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
+import android.util.Log;
import androidx.annotation.VisibleForTesting;
+import com.android.connectivity.resources.R;
+import com.android.server.connectivity.ConnectivityResources;
+
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
+import java.util.HashSet;
import java.util.Optional;
+import java.util.Set;
/** Verifier of the log list signature. */
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
public class SignatureVerifier {
+ private static final String TAG = "SignatureVerifier";
+
private final Context mContext;
@NonNull private Optional<PublicKey> mPublicKey = Optional.empty();
+ private final Set<PublicKey> mAllowedKeys = new HashSet<>();
+
public SignatureVerifier(Context context) {
mContext = context;
}
+ void loadAllowedKeys() {
+ try (InputStream input =
+ new ConnectivityResources(mContext).get().openRawResource(R.raw.ct_public_keys)) {
+ mAllowedKeys.addAll(PemReader.readKeysFrom(input));
+ } catch (GeneralSecurityException | IOException e) {
+ Log.e(TAG, "Error loading public keys", e);
+ }
+ }
+
+ void clearAllowedKeys() {
+ mAllowedKeys.clear();
+ }
+
@VisibleForTesting
Optional<PublicKey> getPublicKey() {
return mPublicKey;
@@ -62,30 +91,75 @@
}
void setPublicKey(String publicKey) throws GeneralSecurityException {
+ byte[] decodedPublicKey = null;
+ try {
+ decodedPublicKey = Base64.getDecoder().decode(publicKey);
+ } catch (IllegalArgumentException e) {
+ throw new GeneralSecurityException("Invalid public key base64 encoding", e);
+ }
setPublicKey(
KeyFactory.getInstance("RSA")
- .generatePublic(
- new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey))));
+ .generatePublic(new X509EncodedKeySpec(decodedPublicKey)));
}
@VisibleForTesting
- void setPublicKey(PublicKey publicKey) {
+ void setPublicKey(PublicKey publicKey) throws GeneralSecurityException {
+ if (!mAllowedKeys.contains(publicKey)) {
+ // TODO(b/400704086): add logging for this failure.
+ throw new GeneralSecurityException("Public key not in allowlist");
+ }
mPublicKey = Optional.of(publicKey);
}
- boolean verify(Uri file, Uri signature)
- throws GeneralSecurityException, IOException, MissingPublicKeyException {
+ LogListUpdateStatus verify(Uri file, Uri signature) {
+ LogListUpdateStatus.Builder statusBuilder = LogListUpdateStatus.builder();
+
if (!mPublicKey.isPresent()) {
- throw new MissingPublicKeyException("Missing public key for signature verification");
+ statusBuilder.setState(PUBLIC_KEY_NOT_FOUND);
+ Log.e(TAG, "No public key found for log list verification");
+ return statusBuilder.build();
}
- Signature verifier = Signature.getInstance("SHA256withRSA");
- verifier.initVerify(mPublicKey.get());
+
ContentResolver contentResolver = mContext.getContentResolver();
try (InputStream fileStream = contentResolver.openInputStream(file);
InputStream signatureStream = contentResolver.openInputStream(signature)) {
+ Signature verifier = Signature.getInstance("SHA256withRSA");
+ verifier.initVerify(mPublicKey.get());
verifier.update(fileStream.readAllBytes());
- return verifier.verify(signatureStream.readAllBytes());
+
+ byte[] signatureBytes = signatureStream.readAllBytes();
+ statusBuilder.setSignature(new String(signatureBytes));
+
+ if (!verifier.verify(Base64.getDecoder().decode(signatureBytes))) {
+ // Leave the UpdateState as UNKNOWN_STATE if successful as there are other
+ // potential failures past the signature verification step
+ statusBuilder.setState(SIGNATURE_VERIFICATION_FAILED);
+ }
+ } catch (IllegalArgumentException e) {
+ Log.w(TAG, "Invalid signature base64 encoding", e);
+ statusBuilder.setState(SIGNATURE_INVALID);
+ return statusBuilder.build();
+ } catch (InvalidKeyException e) {
+ Log.e(TAG, "Key invalid for log list verification", e);
+ statusBuilder.setState(SIGNATURE_INVALID);
+ return statusBuilder.build();
+ } catch (IOException | GeneralSecurityException e) {
+ Log.e(TAG, "Could not verify new log list", e);
+ statusBuilder.setState(SIGNATURE_VERIFICATION_FAILED);
+ return statusBuilder.build();
}
+
+ // Double check if the signature is empty that we set the state correctly
+ if (!statusBuilder.build().hasSignature()) {
+ statusBuilder.setState(SIGNATURE_NOT_FOUND);
+ }
+
+ return statusBuilder.build();
+ }
+
+ @VisibleForTesting
+ boolean addAllowedKey(PublicKey publicKey) {
+ return mAllowedKeys.add(publicKey);
}
}
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
index 2f57fc9..22dc6ab 100644
--- a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
@@ -16,22 +16,14 @@
package com.android.server.net.ct;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_NOT_FOUND;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_VERSION_ALREADY_EXISTS;
-
+import static com.google.common.io.Files.toByteArray;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import static java.nio.charset.StandardCharsets.UTF_8;
-
import android.app.DownloadManager;
import android.app.DownloadManager.Query;
import android.app.DownloadManager.Request;
@@ -43,6 +35,8 @@
import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState;
+
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.After;
@@ -50,6 +44,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -66,6 +61,7 @@
import java.security.PublicKey;
import java.security.Signature;
import java.util.Base64;
+import java.util.Optional;
/** Tests for the {@link CertificateTransparencyDownloader}. */
@RunWith(JUnit4.class)
@@ -73,6 +69,8 @@
@Mock private DownloadManager mDownloadManager;
@Mock private CertificateTransparencyLogger mLogger;
+ private ArgumentCaptor<LogListUpdateStatus> mUpdateStatusCaptor =
+ ArgumentCaptor.forClass(LogListUpdateStatus.class);
private PrivateKey mPrivateKey;
private PublicKey mPublicKey;
@@ -83,6 +81,7 @@
private CertificateTransparencyDownloader mCertificateTransparencyDownloader;
private long mNextDownloadId = 666;
+ private static final long LOG_LIST_TIMESTAMP = 123456789L;
@Before
public void setUp() throws IOException, GeneralSecurityException {
@@ -110,15 +109,16 @@
mContext.getFilesDir());
prepareDownloadManager();
+ mSignatureVerifier.addAllowedKey(mPublicKey);
+ mDataStore.load();
mCertificateTransparencyDownloader.addCompatibilityVersion(mCompatVersion);
- mCertificateTransparencyDownloader.start();
}
@After
public void tearDown() {
mSignatureVerifier.resetPublicKey();
- mCertificateTransparencyDownloader.stop();
mCompatVersion.delete();
+ mDataStore.delete();
}
@Test
@@ -166,6 +166,22 @@
@Test
public void
+ testDownloader_publicKeyDownloadSuccess_publicKeyNotAllowed_doNotStartMetadataDownload()
+ throws Exception {
+ mCertificateTransparencyDownloader.startPublicKeyDownload();
+ PublicKey notAllowed = KeyPairGenerator.getInstance("RSA").generateKeyPair().getPublic();
+
+ assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
+ assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makePublicKeyDownloadCompleteIntent(writePublicKeyToFile(notAllowed)));
+
+ assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
+ assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
+ }
+
+ @Test
+ public void
testDownloader_publicKeyDownloadSuccess_updatePublicKeyFail_doNotStartMetadataDownload()
throws Exception {
mCertificateTransparencyDownloader.startPublicKeyDownload();
@@ -198,45 +214,19 @@
}
@Test
- public void testDownloader_publicKeyDownloadFail_failureThresholdExceeded_logsFailure()
- throws Exception {
+ public void testDownloader_publicKeyDownloadFail_logsFailure() throws Exception {
mCertificateTransparencyDownloader.startPublicKeyDownload();
- // Set the failure count to just below the threshold
- mDataStore.setPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT,
- Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
mCertificateTransparencyDownloader.onReceive(
mContext,
makePublicKeyDownloadFailedIntent(DownloadManager.ERROR_INSUFFICIENT_SPACE));
- assertThat(
- mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
- .isEqualTo(Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD);
verify(mLogger, times(1))
- .logCTLogListUpdateFailedEventWithDownloadStatus(
- DownloadManager.ERROR_INSUFFICIENT_SPACE,
- Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD);
- }
-
- @Test
- public void testDownloader_publicKeyDownloadFail_failureThresholdNotMet_doesNotLog()
- throws Exception {
- mCertificateTransparencyDownloader.startPublicKeyDownload();
- // Set the failure count to well below the threshold
- mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, 0);
-
- mCertificateTransparencyDownloader.onReceive(
- mContext, makePublicKeyDownloadFailedIntent(DownloadManager.ERROR_HTTP_DATA_ERROR));
-
- assertThat(
- mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
- .isEqualTo(1);
- verify(mLogger, never()).logCTLogListUpdateFailedEvent(anyInt(), anyInt());
- verify(mLogger, never()).logCTLogListUpdateFailedEventWithDownloadStatus(
- anyInt(), anyInt());
+ .logCTLogListUpdateStateChangedEvent(
+ LogListUpdateStatus.builder()
+ .setDownloadStatus(
+ Optional.of(DownloadManager.ERROR_INSUFFICIENT_SPACE))
+ .build());
}
@Test
@@ -269,48 +259,20 @@
}
@Test
- public void testDownloader_metadataDownloadFail_failureThresholdExceeded_logsFailure()
- throws Exception {
+ public void testDownloader_metadataDownloadFail_logsFailure() throws Exception {
mCertificateTransparencyDownloader.startMetadataDownload();
- // Set the failure count to just below the threshold
- mDataStore.setPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT,
- Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
mCertificateTransparencyDownloader.onReceive(
mContext,
makeMetadataDownloadFailedIntent(
mCompatVersion, DownloadManager.ERROR_INSUFFICIENT_SPACE));
- assertThat(
- mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
- .isEqualTo(Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD);
verify(mLogger, times(1))
- .logCTLogListUpdateFailedEventWithDownloadStatus(
- DownloadManager.ERROR_INSUFFICIENT_SPACE,
- Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD);
- }
-
- @Test
- public void testDownloader_metadataDownloadFail_failureThresholdNotMet_doesNotLog()
- throws Exception {
- mCertificateTransparencyDownloader.startMetadataDownload();
- // Set the failure count to well below the threshold
- mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, 0);
-
- mCertificateTransparencyDownloader.onReceive(
- mContext,
- makeMetadataDownloadFailedIntent(
- mCompatVersion, DownloadManager.ERROR_INSUFFICIENT_SPACE));
-
- assertThat(
- mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
- .isEqualTo(1);
- verify(mLogger, never()).logCTLogListUpdateFailedEvent(anyInt(), anyInt());
- verify(mLogger, never()).logCTLogListUpdateFailedEventWithDownloadStatus(
- anyInt(), anyInt());
+ .logCTLogListUpdateStateChangedEvent(
+ LogListUpdateStatus.builder()
+ .setDownloadStatus(
+ Optional.of(DownloadManager.ERROR_INSUFFICIENT_SPACE))
+ .build());
}
@Test
@@ -347,48 +309,20 @@
}
@Test
- public void testDownloader_contentDownloadFail_failureThresholdExceeded_logsFailure()
- throws Exception {
+ public void testDownloader_contentDownloadFail_logsFailure() throws Exception {
mCertificateTransparencyDownloader.startContentDownload(mCompatVersion);
- // Set the failure count to just below the threshold
- mDataStore.setPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT,
- Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
mCertificateTransparencyDownloader.onReceive(
mContext,
makeContentDownloadFailedIntent(
mCompatVersion, DownloadManager.ERROR_INSUFFICIENT_SPACE));
- assertThat(
- mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
- .isEqualTo(Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD);
verify(mLogger, times(1))
- .logCTLogListUpdateFailedEventWithDownloadStatus(
- DownloadManager.ERROR_INSUFFICIENT_SPACE,
- Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD);
- }
-
- @Test
- public void testDownloader_contentDownloadFail_failureThresholdNotMet_doesNotLog()
- throws Exception {
- mCertificateTransparencyDownloader.startContentDownload(mCompatVersion);
- // Set the failure count to well below the threshold
- mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, 0);
-
- mCertificateTransparencyDownloader.onReceive(
- mContext,
- makeContentDownloadFailedIntent(
- mCompatVersion, DownloadManager.ERROR_HTTP_DATA_ERROR));
-
- assertThat(
- mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
- .isEqualTo(1);
- verify(mLogger, never()).logCTLogListUpdateFailedEvent(anyInt(), anyInt());
- verify(mLogger, never()).logCTLogListUpdateFailedEventWithDownloadStatus(
- anyInt(), anyInt());
+ .logCTLogListUpdateStateChangedEvent(
+ LogListUpdateStatus.builder()
+ .setDownloadStatus(
+ Optional.of(DownloadManager.ERROR_INSUFFICIENT_SPACE))
+ .build());
}
@Test
@@ -409,17 +343,12 @@
}
@Test
- public void
- testDownloader_contentDownloadSuccess_noSignatureFound_failureThresholdExceeded_logsSingleFailure()
- throws Exception {
+ public void testDownloader_contentDownloadSuccess_noPublicKeyFound_logsSingleFailure()
+ throws Exception {
File logListFile = makeLogListFile("456");
File metadataFile = sign(logListFile);
mSignatureVerifier.setPublicKey(mPublicKey);
mCertificateTransparencyDownloader.startMetadataDownload();
- // Set the failure count to just below the threshold
- mDataStore.setPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT,
- Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
// Set the public key to be missing
mSignatureVerifier.resetPublicKey();
@@ -428,37 +357,24 @@
mCertificateTransparencyDownloader.onReceive(
mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
- assertThat(
- mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
- .isEqualTo(Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD);
verify(mLogger, times(1))
- .logCTLogListUpdateFailedEvent(
- CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_NOT_FOUND,
- Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD);
- verify(mLogger, never())
- .logCTLogListUpdateFailedEvent(
- eq(
- CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION),
- anyInt());
+ .logCTLogListUpdateStateChangedEvent(mUpdateStatusCaptor.capture());
+ assertThat(mUpdateStatusCaptor.getValue().state())
+ .isEqualTo(CTLogListUpdateState.PUBLIC_KEY_NOT_FOUND);
}
@Test
- public void
- testDownloader_contentDownloadSuccess_wrongSignatureAlgo_failureThresholdExceeded_logsSingleFailure()
- throws Exception {
+ public void testDownloader_contentDownloadSuccess_wrongSignatureAlgo_logsSingleFailure()
+ throws Exception {
// Arrange
File logListFile = makeLogListFile("456");
File metadataFile = sign(logListFile);
// Set the key to be deliberately wrong by using diff algorithm
- KeyPairGenerator instance = KeyPairGenerator.getInstance("EC");
- mSignatureVerifier.setPublicKey(instance.generateKeyPair().getPublic());
-
- // Set the failure count to just below the threshold
- mDataStore.setPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT,
- Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
+ PublicKey wrongAlgorithmKey =
+ KeyPairGenerator.getInstance("EC").generateKeyPair().getPublic();
+ mSignatureVerifier.addAllowedKey(wrongAlgorithmKey);
+ mSignatureVerifier.setPublicKey(wrongAlgorithmKey);
// Act
mCertificateTransparencyDownloader.startMetadataDownload();
@@ -468,37 +384,22 @@
mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
// Assert
- assertThat(
- mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
- .isEqualTo(Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD);
- verify(mLogger, never())
- .logCTLogListUpdateFailedEvent(
- eq(
- CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_NOT_FOUND),
- anyInt());
verify(mLogger, times(1))
- .logCTLogListUpdateFailedEvent(
- CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION,
- Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD);
+ .logCTLogListUpdateStateChangedEvent(mUpdateStatusCaptor.capture());
+ assertThat(mUpdateStatusCaptor.getValue().state())
+ .isEqualTo(CTLogListUpdateState.SIGNATURE_INVALID);
}
@Test
- public void
- testDownloader_contentDownloadSuccess_signatureNotVerified_failureThresholdExceeded_logsSingleFailure()
- throws Exception {
+ public void testDownloader_contentDownloadSuccess_signatureNotVerified_logsSingleFailure()
+ throws Exception {
// Arrange
File logListFile = makeLogListFile("456");
- File metadataFile = sign(logListFile);
+ mSignatureVerifier.setPublicKey(mPublicKey);
- // Set the key to be deliberately wrong by using diff key pair
+ // Sign the list with a disallowed key pair
KeyPairGenerator instance = KeyPairGenerator.getInstance("RSA");
- mSignatureVerifier.setPublicKey(instance.generateKeyPair().getPublic());
-
- // Set the failure count to just below the threshold
- mDataStore.setPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT,
- Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
+ File metadataFile = sign(logListFile, instance.generateKeyPair().getPrivate());
// Act
mCertificateTransparencyDownloader.startMetadataDownload();
@@ -508,66 +409,19 @@
mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
// Assert
- assertThat(
- mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
- .isEqualTo(Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD);
- verify(mLogger, never())
- .logCTLogListUpdateFailedEvent(
- eq(
- CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_NOT_FOUND),
- anyInt());
verify(mLogger, times(1))
- .logCTLogListUpdateFailedEvent(
- CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION,
- Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD);
+ .logCTLogListUpdateStateChangedEvent(mUpdateStatusCaptor.capture());
+ LogListUpdateStatus statusValue = mUpdateStatusCaptor.getValue();
+ assertThat(statusValue.state())
+ .isEqualTo(CTLogListUpdateState.SIGNATURE_VERIFICATION_FAILED);
+ assertThat(statusValue.signature()).isEqualTo(new String(toByteArray(metadataFile)));
}
@Test
- public void
- testDownloader_contentDownloadSuccess_wrongSignature_failureThresholdNotMet_doesNotLog()
- throws Exception {
- File logListFile = makeLogListFile("456");
- File metadataFile = sign(logListFile);
- // Set the key to be deliberately wrong by using diff key pair
- KeyPairGenerator instance = KeyPairGenerator.getInstance("RSA");
- mSignatureVerifier.setPublicKey(instance.generateKeyPair().getPublic());
- // Set the failure count to well below the threshold
- mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, 0);
-
- mCertificateTransparencyDownloader.startMetadataDownload();
- mCertificateTransparencyDownloader.onReceive(
- mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
- mCertificateTransparencyDownloader.onReceive(
- mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
-
- assertThat(
- mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
- .isEqualTo(1);
- verify(mLogger, never())
- .logCTLogListUpdateFailedEvent(
- eq(
- CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_NOT_FOUND),
- anyInt());
- verify(mLogger, never())
- .logCTLogListUpdateFailedEvent(
- eq(
- CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION),
- anyInt());
- }
-
- @Test
- public void
- testDownloader_contentDownloadSuccess_installFail_failureThresholdExceeded_logsFailure()
- throws Exception {
+ public void testDownloader_contentDownloadSuccess_installFail_logsFailure() throws Exception {
File invalidLogListFile = writeToFile("not_a_json_log_list".getBytes());
File metadataFile = sign(invalidLogListFile);
mSignatureVerifier.setPublicKey(mPublicKey);
- // Set the failure count to just below the threshold
- mDataStore.setPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT,
- Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
mCertificateTransparencyDownloader.startMetadataDownload();
mCertificateTransparencyDownloader.onReceive(
@@ -575,39 +429,11 @@
mCertificateTransparencyDownloader.onReceive(
mContext, makeContentDownloadCompleteIntent(mCompatVersion, invalidLogListFile));
- assertThat(
- mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
- .isEqualTo(Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD);
verify(mLogger, times(1))
- .logCTLogListUpdateFailedEvent(
- CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_VERSION_ALREADY_EXISTS,
- Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD);
- }
-
- @Test
- public void
- testDownloader_contentDownloadSuccess_installFail_failureThresholdNotMet_doesNotLog()
- throws Exception {
- File invalidLogListFile = writeToFile("not_a_json_log_list".getBytes());
- File metadataFile = sign(invalidLogListFile);
- mSignatureVerifier.setPublicKey(mPublicKey);
- // Set the failure count to well below the threshold
- mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, 0);
-
- mCertificateTransparencyDownloader.startMetadataDownload();
- mCertificateTransparencyDownloader.onReceive(
- mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
- mCertificateTransparencyDownloader.onReceive(
- mContext, makeContentDownloadCompleteIntent(mCompatVersion, invalidLogListFile));
-
- assertThat(
- mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
- .isEqualTo(1);
- verify(mLogger, never()).logCTLogListUpdateFailedEvent(anyInt(), anyInt());
- verify(mLogger, never()).logCTLogListUpdateFailedEventWithDownloadStatus(
- anyInt(), anyInt());
+ .logCTLogListUpdateStateChangedEvent(mUpdateStatusCaptor.capture());
+ LogListUpdateStatus statusValue = mUpdateStatusCaptor.getValue();
+ assertThat(statusValue.state()).isEqualTo(CTLogListUpdateState.LOG_LIST_INVALID);
+ assertThat(statusValue.signature()).isEqualTo(new String(toByteArray(metadataFile)));
}
@Test
@@ -645,7 +471,8 @@
}
@Test
- public void testDownloader_endToEndSuccess_installNewVersion() throws Exception {
+ public void testDownloader_endToEndSuccess_installNewVersion_andLogsSuccess() throws Exception {
+ // Arrange
String newVersion = "456";
File logListFile = makeLogListFile(newVersion);
File metadataFile = sign(logListFile);
@@ -653,6 +480,7 @@
assertNoVersionIsInstalled();
+ // Act
// 1. Start download of public key.
mCertificateTransparencyDownloader.startPublicKeyDownload();
@@ -670,7 +498,15 @@
mCertificateTransparencyDownloader.onReceive(
mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
+ // Assert
assertInstallSuccessful(newVersion);
+ verify(mLogger, times(1))
+ .logCTLogListUpdateStateChangedEvent(mUpdateStatusCaptor.capture());
+
+ LogListUpdateStatus statusValue = mUpdateStatusCaptor.getValue();
+ assertThat(statusValue.state()).isEqualTo(CTLogListUpdateState.SUCCESS);
+ assertThat(statusValue.signature()).isEqualTo(new String(toByteArray(metadataFile)));
+ assertThat(statusValue.logListTimestamp()).isEqualTo(LOG_LIST_TIMESTAMP);
}
private void assertNoVersionIsInstalled() {
@@ -779,21 +615,30 @@
File logListFile = File.createTempFile("log_list", "json");
try (OutputStream outputStream = new FileOutputStream(logListFile)) {
- outputStream.write(new JSONObject().put("version", version).toString().getBytes(UTF_8));
+ JSONObject contentJson =
+ new JSONObject()
+ .put("version", version)
+ .put("log_list_timestamp", LOG_LIST_TIMESTAMP);
+ outputStream.write(contentJson.toString().getBytes());
}
return logListFile;
}
private File sign(File file) throws IOException, GeneralSecurityException {
+ return sign(file, mPrivateKey);
+ }
+
+ private File sign(File file, PrivateKey privateKey)
+ throws IOException, GeneralSecurityException {
File signatureFile = File.createTempFile("log_list-metadata", "sig");
Signature signer = Signature.getInstance("SHA256withRSA");
- signer.initSign(mPrivateKey);
+ signer.initSign(privateKey);
try (InputStream fileStream = new FileInputStream(file);
OutputStream outputStream = new FileOutputStream(signatureFile)) {
signer.update(fileStream.readAllBytes());
- outputStream.write(signer.sign());
+ outputStream.write(Base64.getEncoder().encode(signer.sign()));
}
return signatureFile;
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java
index 38fff48..2b8b3cd 100644
--- a/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java
@@ -15,6 +15,11 @@
*/
package com.android.server.net.ct;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.LOG_LIST_INVALID;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SUCCESS;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.UNKNOWN_STATE;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.VERSION_ALREADY_EXISTS;
+
import static com.google.common.truth.Truth.assertThat;
import androidx.test.platform.app.InstrumentationRegistry;
@@ -37,6 +42,8 @@
public class CompatibilityVersionTest {
private static final String TEST_VERSION = "v123";
+ private static final long LOG_LIST_TIMESTAMP = 123456789L;
+ private static final String SIGNATURE = "fake_signature";
private final File mTestDir =
InstrumentationRegistry.getInstrumentation().getContext().getFilesDir();
@@ -52,6 +59,7 @@
@Test
public void testCompatibilityVersion_versionDirectory_setupSuccessful() {
File versionDir = mCompatVersion.getVersionDir();
+
assertThat(versionDir.exists()).isFalse();
assertThat(versionDir.getAbsolutePath()).startsWith(mTestDir.getAbsolutePath());
assertThat(versionDir.getAbsolutePath()).endsWith(TEST_VERSION);
@@ -60,6 +68,7 @@
@Test
public void testCompatibilityVersion_symlink_setupSuccessful() {
File dirSymlink = mCompatVersion.getLogsDirSymlink();
+
assertThat(dirSymlink.exists()).isFalse();
assertThat(dirSymlink.getAbsolutePath())
.startsWith(mCompatVersion.getVersionDir().getAbsolutePath());
@@ -68,18 +77,44 @@
@Test
public void testCompatibilityVersion_logsFile_setupSuccessful() {
File logsFile = mCompatVersion.getLogsFile();
+
assertThat(logsFile.exists()).isFalse();
assertThat(logsFile.getAbsolutePath())
.startsWith(mCompatVersion.getLogsDirSymlink().getAbsolutePath());
}
@Test
+ public void testCompatibilityVersion_installSuccessful_keepsStatusDetails() throws Exception {
+ String version = "i_am_version";
+ JSONObject logList = makeLogList(version, "i_am_content");
+
+ try (InputStream inputStream = asStream(logList)) {
+ assertThat(
+ mCompatVersion.install(
+ inputStream,
+ LogListUpdateStatus.builder()
+ .setSignature(SIGNATURE)
+ .setState(UNKNOWN_STATE)))
+ .isEqualTo(
+ LogListUpdateStatus.builder()
+ .setSignature(SIGNATURE)
+ .setLogListTimestamp(LOG_LIST_TIMESTAMP)
+ // Ensure the state is correctly overridden to SUCCESS
+ .setState(SUCCESS)
+ .build());
+ }
+ }
+
+ @Test
public void testCompatibilityVersion_installSuccessful() throws Exception {
String version = "i_am_version";
JSONObject logList = makeLogList(version, "i_am_content");
try (InputStream inputStream = asStream(logList)) {
- assertThat(mCompatVersion.install(inputStream)).isTrue();
+ assertThat(
+ mCompatVersion.install(
+ inputStream, LogListUpdateStatus.builder()))
+ .isEqualTo(getSuccessfulUpdateStatus());
}
File logListFile = mCompatVersion.getLogsFile();
@@ -107,7 +142,10 @@
@Test
public void testCompatibilityVersion_deleteSuccessfully() throws Exception {
try (InputStream inputStream = asStream(makeLogList(/* version= */ "123"))) {
- assertThat(mCompatVersion.install(inputStream)).isTrue();
+ assertThat(
+ mCompatVersion.install(
+ inputStream, LogListUpdateStatus.builder()))
+ .isEqualTo(getSuccessfulUpdateStatus());
}
mCompatVersion.delete();
@@ -118,7 +156,10 @@
@Test
public void testCompatibilityVersion_invalidLogList() throws Exception {
try (InputStream inputStream = new ByteArrayInputStream(("not_a_valid_list".getBytes()))) {
- assertThat(mCompatVersion.install(inputStream)).isFalse();
+ assertThat(
+ mCompatVersion.install(
+ inputStream, LogListUpdateStatus.builder()))
+ .isEqualTo(LogListUpdateStatus.builder().setState(LOG_LIST_INVALID).build());
}
assertThat(mCompatVersion.getLogsFile().exists()).isFalse();
@@ -138,7 +179,10 @@
JSONObject newLogList = makeLogList(existingVersion, "i_am_the_real_content");
try (InputStream inputStream = asStream(newLogList)) {
- assertThat(mCompatVersion.install(inputStream)).isTrue();
+ assertThat(
+ mCompatVersion.install(
+ inputStream, LogListUpdateStatus.builder()))
+ .isEqualTo(getSuccessfulUpdateStatus());
}
assertThat(readAsString(logsListFile)).isEqualTo(newLogList.toString());
@@ -149,11 +193,21 @@
String existingVersion = "666";
JSONObject existingLogList = makeLogList(existingVersion, "i_was_installed_successfully");
try (InputStream inputStream = asStream(existingLogList)) {
- assertThat(mCompatVersion.install(inputStream)).isTrue();
+ assertThat(
+ mCompatVersion.install(
+ inputStream, LogListUpdateStatus.builder()))
+ .isEqualTo(getSuccessfulUpdateStatus());
}
try (InputStream inputStream = asStream(makeLogList(existingVersion, "i_am_ignored"))) {
- assertThat(mCompatVersion.install(inputStream)).isFalse();
+ assertThat(
+ mCompatVersion.install(
+ inputStream, LogListUpdateStatus.builder()))
+ .isEqualTo(
+ LogListUpdateStatus.builder()
+ .setState(VERSION_ALREADY_EXISTS)
+ .setLogListTimestamp(LOG_LIST_TIMESTAMP)
+ .build());
}
assertThat(readAsString(mCompatVersion.getLogsFile()))
@@ -165,13 +219,22 @@
}
private static JSONObject makeLogList(String version) throws JSONException {
- return new JSONObject().put("version", version);
+ return new JSONObject()
+ .put("version", version)
+ .put("log_list_timestamp", LOG_LIST_TIMESTAMP);
}
private static JSONObject makeLogList(String version, String content) throws JSONException {
return makeLogList(version).put("content", content);
}
+ private static LogListUpdateStatus getSuccessfulUpdateStatus() {
+ return LogListUpdateStatus.builder()
+ .setState(SUCCESS)
+ .setLogListTimestamp(LOG_LIST_TIMESTAMP)
+ .build();
+ }
+
private static String readAsString(File file) throws IOException {
try (InputStream in = new FileInputStream(file)) {
return new String(in.readAllBytes());
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/PemReaderTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/PemReaderTest.java
new file mode 100644
index 0000000..08629db
--- /dev/null
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/PemReaderTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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.ct;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.KeyPairGenerator;
+import java.security.PublicKey;
+import java.util.Base64;
+
+/** Tests for the {@link PemReader}. */
+@RunWith(JUnit4.class)
+public class PemReaderTest {
+
+ @Test
+ public void testReadKeys_singleKey() throws GeneralSecurityException, IOException {
+ PublicKey key = KeyPairGenerator.getInstance("RSA").generateKeyPair().getPublic();
+
+ assertThat(PemReader.readKeysFrom(toInputStream(key))).containsExactly(key);
+ }
+
+ @Test
+ public void testReadKeys_multipleKeys() throws GeneralSecurityException, IOException {
+ KeyPairGenerator instance = KeyPairGenerator.getInstance("RSA");
+ PublicKey key1 = instance.generateKeyPair().getPublic();
+ PublicKey key2 = instance.generateKeyPair().getPublic();
+
+ assertThat(PemReader.readKeysFrom(toInputStream(key1, key2))).containsExactly(key1, key2);
+ }
+
+ @Test
+ public void testReadKeys_notSupportedKeyType() throws GeneralSecurityException {
+ PublicKey key = KeyPairGenerator.getInstance("EC").generateKeyPair().getPublic();
+
+ assertThrows(
+ GeneralSecurityException.class, () -> PemReader.readKeysFrom(toInputStream(key)));
+ }
+
+ @Test
+ public void testReadKeys_notBase64EncodedKey() throws GeneralSecurityException {
+ InputStream inputStream =
+ new ByteArrayInputStream(
+ (""
+ + "-----BEGIN PUBLIC KEY-----\n"
+ + KeyPairGenerator.getInstance("RSA")
+ .generateKeyPair()
+ .getPublic()
+ .toString()
+ + "\n-----END PUBLIC KEY-----\n")
+ .getBytes());
+
+ assertThrows(GeneralSecurityException.class, () -> PemReader.readKeysFrom(inputStream));
+ }
+
+ @Test
+ public void testReadKeys_noPemBegin() throws GeneralSecurityException {
+ PublicKey key = KeyPairGenerator.getInstance("RSA").generateKeyPair().getPublic();
+ String base64Key = Base64.getEncoder().encodeToString(key.getEncoded());
+ String pemNoBegin = base64Key + "\n-----END PUBLIC KEY-----\n";
+
+ assertThrows(
+ IOException.class,
+ () -> PemReader.readKeysFrom(new ByteArrayInputStream(pemNoBegin.getBytes())));
+ }
+
+ @Test
+ public void testReadKeys_noPemEnd() throws GeneralSecurityException {
+ PublicKey key = KeyPairGenerator.getInstance("RSA").generateKeyPair().getPublic();
+ String base64Key = Base64.getEncoder().encodeToString(key.getEncoded());
+ String pemNoEnd = "-----BEGIN PUBLIC KEY-----\n" + base64Key;
+
+ assertThrows(
+ IOException.class,
+ () -> PemReader.readKeysFrom(new ByteArrayInputStream(pemNoEnd.getBytes())));
+ }
+
+ private InputStream toInputStream(PublicKey... keys) {
+ StringBuilder builder = new StringBuilder();
+
+ for (PublicKey key : keys) {
+ builder.append("-----BEGIN PUBLIC KEY-----\n")
+ .append(Base64.getEncoder().encodeToString(key.getEncoded()))
+ .append("\n-----END PUBLIC KEY-----\n");
+ }
+
+ return new ByteArrayInputStream(builder.toString().getBytes());
+ }
+}
diff --git a/remoteauth/OWNERS b/remoteauth/OWNERS
index 25a32b9..ee46c1c 100644
--- a/remoteauth/OWNERS
+++ b/remoteauth/OWNERS
@@ -2,7 +2,6 @@
# Bug template url: http://b/new?component=1145231&template=1715387
billyhuang@google.com
boetger@google.com
-casbor@google.com
derekjedral@google.com
dlm@google.com
igorzas@google.com
diff --git a/remoteauth/service/jni/Android.bp b/remoteauth/service/jni/Android.bp
index 57e3ec9..c7ad738 100644
--- a/remoteauth/service/jni/Android.bp
+++ b/remoteauth/service/jni/Android.bp
@@ -24,9 +24,6 @@
"libasync_trait",
],
prefer_rlib: true,
- apex_available: [
- "com.android.remoteauth",
- ],
host_supported: true,
}
diff --git a/service-t/Android.bp b/service-t/Android.bp
index d2e2a80..ab38c7a 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -29,6 +29,7 @@
name: "service-connectivity-tiramisu-sources",
srcs: [
"src/**/*.java",
+ ":vcn-location-sources",
],
visibility: ["//visibility:private"],
}
diff --git a/service-t/src/com/android/server/ConnectivityServiceInitializer.java b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
index 5d23fdc..5ef1aef 100644
--- a/service-t/src/com/android/server/ConnectivityServiceInitializer.java
+++ b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
@@ -30,6 +30,9 @@
import com.android.server.nearby.NearbyService;
import com.android.server.net.ct.CertificateTransparencyService;
import com.android.server.thread.ThreadNetworkService;
+import com.android.server.vcn.VcnLocation;
+
+import java.lang.reflect.Constructor;
/**
* Connectivity service initializer for core networking. This is called by system server to create
@@ -37,6 +40,9 @@
*/
public final class ConnectivityServiceInitializer extends SystemService {
private static final String TAG = ConnectivityServiceInitializer.class.getSimpleName();
+ private static final String CONNECTIVITY_SERVICE_INITIALIZER_B_CLASS =
+ "com.android.server.ConnectivityServiceInitializerB";
+
private final ConnectivityNativeService mConnectivityNative;
private final ConnectivityService mConnectivity;
private final IpSecService mIpSecService;
@@ -45,6 +51,7 @@
private final EthernetServiceImpl mEthernetServiceImpl;
private final ThreadNetworkService mThreadNetworkService;
private final CertificateTransparencyService mCertificateTransparencyService;
+ private final SystemService mConnectivityServiceInitializerB;
public ConnectivityServiceInitializer(Context context) {
super(context);
@@ -58,6 +65,7 @@
mNearbyService = createNearbyService(context);
mThreadNetworkService = createThreadNetworkService(context);
mCertificateTransparencyService = createCertificateTransparencyService(context);
+ mConnectivityServiceInitializerB = createConnectivityServiceInitializerB(context);
}
@Override
@@ -99,6 +107,11 @@
publishBinderService(ThreadNetworkManager.SERVICE_NAME, mThreadNetworkService,
/* allowIsolated= */ false);
}
+
+ if (mConnectivityServiceInitializerB != null) {
+ Log.i(TAG, "ConnectivityServiceInitializerB#onStart");
+ mConnectivityServiceInitializerB.onStart();
+ }
}
@Override
@@ -118,6 +131,10 @@
if (SdkLevel.isAtLeastV() && mCertificateTransparencyService != null) {
mCertificateTransparencyService.onBootPhase(phase);
}
+
+ if (mConnectivityServiceInitializerB != null) {
+ mConnectivityServiceInitializerB.onBootPhase(phase);
+ }
}
/**
@@ -202,4 +219,28 @@
? new CertificateTransparencyService(context)
: null;
}
+
+ // TODO: b/374174952 After VCN code is moved to the Connectivity folder, merge
+ // ConnectivityServiceInitializerB into ConnectivityServiceInitializer and directly create and
+ // register VcnManagementService in ConnectivityServiceInitializer
+ /** Return ConnectivityServiceInitializerB instance if enable, otherwise null. */
+ @Nullable
+ private SystemService createConnectivityServiceInitializerB(Context context) {
+ if (!VcnLocation.IS_VCN_IN_MAINLINE || !SdkLevel.isAtLeastB()) {
+ return null;
+ }
+
+ try {
+ final Class<?> connectivityServiceInitializerBClass =
+ Class.forName(CONNECTIVITY_SERVICE_INITIALIZER_B_CLASS);
+ final Constructor constructor =
+ connectivityServiceInitializerBClass.getConstructor(Context.class);
+
+ return (SystemService) constructor.newInstance(context);
+ } catch (Exception e) {
+ Log.e(TAG, "Fail to load ConnectivityServiceInitializerB " + e);
+ }
+
+ return null;
+ }
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/DiscoveryExecutor.java b/service-t/src/com/android/server/connectivity/mdns/DiscoveryExecutor.java
new file mode 100644
index 0000000..21af1a1
--- /dev/null
+++ b/service-t/src/com/android/server/connectivity/mdns/DiscoveryExecutor.java
@@ -0,0 +1,140 @@
+/*
+ * 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.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.util.Log;
+import android.util.Pair;
+
+import androidx.annotation.GuardedBy;
+
+import com.android.net.module.util.HandlerUtils;
+
+import java.util.ArrayList;
+import java.util.concurrent.Executor;
+
+/**
+ * A utility class to generate a handler, optionally with a looper, and to run functions on the
+ * newly created handler.
+ */
+public class DiscoveryExecutor implements Executor {
+ private static final String TAG = DiscoveryExecutor.class.getSimpleName();
+ @Nullable
+ private final HandlerThread mHandlerThread;
+
+ @GuardedBy("mPendingTasks")
+ @Nullable
+ private Handler mHandler;
+ // Store pending tasks and associated delay time. Each Pair represents a pending task
+ // (first) and its delay time (second).
+ @GuardedBy("mPendingTasks")
+ @NonNull
+ private final ArrayList<Pair<Runnable, Long>> mPendingTasks = new ArrayList<>();
+
+ DiscoveryExecutor(@Nullable Looper defaultLooper) {
+ if (defaultLooper != null) {
+ this.mHandlerThread = null;
+ synchronized (mPendingTasks) {
+ this.mHandler = new Handler(defaultLooper);
+ }
+ } else {
+ this.mHandlerThread = new HandlerThread(MdnsDiscoveryManager.class.getSimpleName()) {
+ @Override
+ protected void onLooperPrepared() {
+ synchronized (mPendingTasks) {
+ mHandler = new Handler(getLooper());
+ for (Pair<Runnable, Long> pendingTask : mPendingTasks) {
+ mHandler.postDelayed(pendingTask.first, pendingTask.second);
+ }
+ mPendingTasks.clear();
+ }
+ }
+ };
+ this.mHandlerThread.start();
+ }
+ }
+
+ /**
+ * Check if the current thread is the expected thread. If it is, run the given function.
+ * Otherwise, execute it using the handler.
+ */
+ public void checkAndRunOnHandlerThread(@NonNull Runnable function) {
+ if (this.mHandlerThread == null) {
+ // Callers are expected to already be running on the handler when a defaultLooper
+ // was provided
+ function.run();
+ } else {
+ execute(function);
+ }
+ }
+
+ /** Execute the given function */
+ @Override
+ public void execute(Runnable function) {
+ executeDelayed(function, 0L /* delayMillis */);
+ }
+
+ /** Execute the given function after the specified amount of time elapses. */
+ public void executeDelayed(Runnable function, long delayMillis) {
+ final Handler handler;
+ synchronized (mPendingTasks) {
+ if (this.mHandler == null) {
+ mPendingTasks.add(Pair.create(function, delayMillis));
+ return;
+ } else {
+ handler = this.mHandler;
+ }
+ }
+ handler.postDelayed(function, delayMillis);
+ }
+
+ /** Shutdown the thread if necessary. */
+ public void shutDown() {
+ if (this.mHandlerThread != null) {
+ this.mHandlerThread.quitSafely();
+ }
+ }
+
+ /**
+ * Ensures that the current running thread is the same as the handler thread.
+ */
+ public void ensureRunningOnHandlerThread() {
+ synchronized (mPendingTasks) {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+ }
+ }
+
+ /**
+ * Runs the specified task synchronously for dump method.
+ */
+ public void runWithScissorsForDumpIfReady(@NonNull Runnable function) {
+ final Handler handler;
+ synchronized (mPendingTasks) {
+ if (this.mHandler == null) {
+ Log.d(TAG, "The handler is not ready. Ignore the DiscoveryManager dump");
+ return;
+ } else {
+ handler = this.mHandler;
+ }
+ }
+ HandlerUtils.runWithScissorsForDump(handler, function, 10_000);
+ }
+}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
index 54f7ca3..bd00b70 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -35,6 +35,7 @@
import android.os.Looper;
import android.text.TextUtils;
import android.util.ArrayMap;
+import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;
@@ -93,6 +94,7 @@
new ArrayMap<>();
private final MdnsFeatureFlags mMdnsFeatureFlags;
private final Map<String, Integer> mServiceTypeToOffloadPriority;
+ private final ArraySet<String> mOffloadServiceTypeDenyList;
/**
* Dependencies for {@link MdnsAdvertiser}, useful for testing.
@@ -160,6 +162,16 @@
return mInterfaceOffloadServices.getOrDefault(interfaceName, Collections.emptyList());
}
+ private boolean isInOffloadDenyList(@NonNull String serviceType) {
+ for (int i = 0; i < mOffloadServiceTypeDenyList.size(); ++i) {
+ final String denyListServiceType = mOffloadServiceTypeDenyList.valueAt(i);
+ if (DnsUtils.equalsIgnoreDnsCase(serviceType, denyListServiceType)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private final MdnsInterfaceAdvertiser.Callback mInterfaceAdvertiserCb =
new MdnsInterfaceAdvertiser.Callback() {
@Override
@@ -173,19 +185,25 @@
if (mMdnsFeatureFlags.mIsMdnsOffloadFeatureEnabled
// TODO: Enable offload when the serviceInfo contains a custom host.
&& TextUtils.isEmpty(registration.getServiceInfo().getHostname())) {
- final String interfaceName = advertiser.getSocketInterfaceName();
- final List<OffloadServiceInfoWrapper> existingOffloadServiceInfoWrappers =
- mInterfaceOffloadServices.computeIfAbsent(interfaceName,
- k -> new ArrayList<>());
- // Remove existing offload services from cache for update.
- existingOffloadServiceInfoWrappers.removeIf(item -> item.mServiceId == serviceId);
+ final String serviceType = registration.getServiceInfo().getServiceType();
+ if (isInOffloadDenyList(serviceType)) {
+ mSharedLog.i("Offload denied for service type: " + serviceType);
+ } else {
+ final String interfaceName = advertiser.getSocketInterfaceName();
+ final List<OffloadServiceInfoWrapper> existingOffloadServiceInfoWrappers =
+ mInterfaceOffloadServices.computeIfAbsent(interfaceName,
+ k -> new ArrayList<>());
+ // Remove existing offload services from cache for update.
+ existingOffloadServiceInfoWrappers.removeIf(
+ item -> item.mServiceId == serviceId);
- byte[] rawOffloadPacket = advertiser.getRawOffloadPayload(serviceId);
- final OffloadServiceInfoWrapper newOffloadServiceInfoWrapper = createOffloadService(
- serviceId, registration, rawOffloadPacket);
- existingOffloadServiceInfoWrappers.add(newOffloadServiceInfoWrapper);
- mCb.onOffloadStartOrUpdate(interfaceName,
- newOffloadServiceInfoWrapper.mOffloadServiceInfo);
+ byte[] rawOffloadPacket = advertiser.getRawOffloadPayload(serviceId);
+ final OffloadServiceInfoWrapper newOffloadServiceInfoWrapper =
+ createOffloadService(serviceId, registration, rawOffloadPacket);
+ existingOffloadServiceInfoWrappers.add(newOffloadServiceInfoWrapper);
+ mCb.onOffloadStartOrUpdate(interfaceName,
+ newOffloadServiceInfoWrapper.mOffloadServiceInfo);
+ }
}
// Wait for all current interfaces to be done probing before notifying of success.
@@ -846,6 +864,8 @@
final ConnectivityResources res = new ConnectivityResources(context);
mServiceTypeToOffloadPriority = parseOffloadPriorityList(
res.get().getStringArray(R.array.config_nsdOffloadServicesPriority), sharedLog);
+ mOffloadServiceTypeDenyList = new ArraySet<>(
+ res.get().getStringArray(R.array.config_nsdOffloadServicesDenyList));
}
private static Map<String, Integer> parseOffloadPriorityList(
@@ -1007,6 +1027,19 @@
});
}
+ private List<String> getOffloadSubtype(@NonNull NsdServiceInfo nsdServiceInfo) {
+ // Workaround: Google Cast doesn't announce subtypes per DNS-SD/mDNS spec.
+ // Thus, subtypes aren't offloaded; only "_googlecast._tcp" is.
+ // Subtype responses occur when hardware offload is off.
+ // This solution works because Google Cast doesn't follow the intended usage of subtypes in
+ // the spec, as it always discovers for both the subtype+base type, and only uses the mDNS
+ // subtype as an optimization.
+ if (nsdServiceInfo.getServiceType().equals("_googlecast._tcp")) {
+ return new ArrayList<>();
+ }
+ return new ArrayList<>(nsdServiceInfo.getSubtypes());
+ }
+
private OffloadServiceInfoWrapper createOffloadService(int serviceId,
@NonNull Registration registration, byte[] rawOffloadPacket) {
final NsdServiceInfo nsdServiceInfo = registration.getServiceInfo();
@@ -1017,7 +1050,7 @@
final OffloadServiceInfo offloadServiceInfo = new OffloadServiceInfo(
new OffloadServiceInfo.Key(nsdServiceInfo.getServiceName(),
nsdServiceInfo.getServiceType()),
- new ArrayList<>(nsdServiceInfo.getSubtypes()),
+ getOffloadSubtype(nsdServiceInfo),
String.join(".", mDeviceHostName),
rawOffloadPacket,
priority,
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
index c833422..33bcb70 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
@@ -16,24 +16,17 @@
package com.android.server.connectivity.mdns;
-import static com.android.internal.annotations.VisibleForTesting.Visibility;
-
import android.Manifest.permission;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
-import android.os.Handler;
-import android.os.HandlerThread;
import android.os.Looper;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Pair;
-import androidx.annotation.GuardedBy;
-
import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.DnsUtils;
-import com.android.net.module.util.HandlerUtils;
import com.android.net.module.util.SharedLog;
import java.io.IOException;
@@ -41,7 +34,6 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
-import java.util.concurrent.Executor;
/**
* This class keeps tracking the set of registered {@link MdnsServiceBrowserListener} instances, and
@@ -137,98 +129,6 @@
}
/**
- * A utility class to generate a handler, optionally with a looper, and to run functions on the
- * newly created handler.
- */
- @VisibleForTesting(visibility = Visibility.PRIVATE)
- static class DiscoveryExecutor implements Executor {
- private final HandlerThread handlerThread;
-
- @GuardedBy("pendingTasks")
- @Nullable private Handler handler;
- // Store pending tasks and associated delay time. Each Pair represents a pending task
- // (first) and its delay time (second).
- @GuardedBy("pendingTasks")
- @NonNull private final ArrayList<Pair<Runnable, Long>> pendingTasks = new ArrayList<>();
-
- DiscoveryExecutor(@Nullable Looper defaultLooper) {
- if (defaultLooper != null) {
- this.handlerThread = null;
- synchronized (pendingTasks) {
- this.handler = new Handler(defaultLooper);
- }
- } else {
- this.handlerThread = new HandlerThread(MdnsDiscoveryManager.class.getSimpleName()) {
- @Override
- protected void onLooperPrepared() {
- synchronized (pendingTasks) {
- handler = new Handler(getLooper());
- for (Pair<Runnable, Long> pendingTask : pendingTasks) {
- handler.postDelayed(pendingTask.first, pendingTask.second);
- }
- pendingTasks.clear();
- }
- }
- };
- this.handlerThread.start();
- }
- }
-
- public void checkAndRunOnHandlerThread(@NonNull Runnable function) {
- if (this.handlerThread == null) {
- // Callers are expected to already be running on the handler when a defaultLooper
- // was provided
- function.run();
- } else {
- execute(function);
- }
- }
-
- @Override
- public void execute(Runnable function) {
- executeDelayed(function, 0L /* delayMillis */);
- }
-
- public void executeDelayed(Runnable function, long delayMillis) {
- final Handler handler;
- synchronized (pendingTasks) {
- if (this.handler == null) {
- pendingTasks.add(Pair.create(function, delayMillis));
- return;
- } else {
- handler = this.handler;
- }
- }
- handler.postDelayed(function, delayMillis);
- }
-
- void shutDown() {
- if (this.handlerThread != null) {
- this.handlerThread.quitSafely();
- }
- }
-
- void ensureRunningOnHandlerThread() {
- synchronized (pendingTasks) {
- HandlerUtils.ensureRunningOnHandlerThread(handler);
- }
- }
-
- public void runWithScissorsForDumpIfReady(@NonNull Runnable function) {
- final Handler handler;
- synchronized (pendingTasks) {
- if (this.handler == null) {
- Log.d(TAG, "The handler is not ready. Ignore the DiscoveryManager dump");
- return;
- } else {
- handler = this.handler;
- }
- }
- HandlerUtils.runWithScissorsForDump(handler, function, 10_000);
- }
- }
-
- /**
* Do the cleanup of the MdnsDiscoveryManager
*/
public void shutDown() {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
index 2f3bdc5..1cf5e4d 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
@@ -95,6 +95,11 @@
"nsd_cached_services_retention_time";
public static final int DEFAULT_CACHED_SERVICES_RETENTION_TIME_MILLISECONDS = 10000;
+ /**
+ * A feature flag to control whether the accurate delay callback should be enabled.
+ */
+ public static final String NSD_ACCURATE_DELAY_CALLBACK = "nsd_accurate_delay_callback";
+
// Flag for offload feature
public final boolean mIsMdnsOffloadFeatureEnabled;
@@ -128,6 +133,9 @@
// Retention Time for cached services
public final long mCachedServicesRetentionTime;
+ // Flag for accurate delay callback
+ public final boolean mIsAccurateDelayCallbackEnabled;
+
// Flag to use shorter (16 characters + .local) hostnames
public final boolean mIsShortHostnamesEnabled;
@@ -231,6 +239,14 @@
}
/**
+ * Indicates whether {@link #NSD_ACCURATE_DELAY_CALLBACK} is enabled, including for testing.
+ */
+ public boolean isAccurateDelayCallbackEnabled() {
+ return mIsAccurateDelayCallbackEnabled
+ || isForceEnabledForTest(NSD_ACCURATE_DELAY_CALLBACK);
+ }
+
+ /**
* The constructor for {@link MdnsFeatureFlags}.
*/
public MdnsFeatureFlags(boolean isOffloadFeatureEnabled,
@@ -244,6 +260,7 @@
boolean avoidAdvertisingEmptyTxtRecords,
boolean isCachedServicesRemovalEnabled,
long cachedServicesRetentionTime,
+ boolean isAccurateDelayCallbackEnabled,
boolean isShortHostnamesEnabled,
@Nullable FlagOverrideProvider overrideProvider) {
mIsMdnsOffloadFeatureEnabled = isOffloadFeatureEnabled;
@@ -257,6 +274,7 @@
mAvoidAdvertisingEmptyTxtRecords = avoidAdvertisingEmptyTxtRecords;
mIsCachedServicesRemovalEnabled = isCachedServicesRemovalEnabled;
mCachedServicesRetentionTime = cachedServicesRetentionTime;
+ mIsAccurateDelayCallbackEnabled = isAccurateDelayCallbackEnabled;
mIsShortHostnamesEnabled = isShortHostnamesEnabled;
mOverrideProvider = overrideProvider;
}
@@ -281,6 +299,7 @@
private boolean mAvoidAdvertisingEmptyTxtRecords;
private boolean mIsCachedServicesRemovalEnabled;
private long mCachedServicesRetentionTime;
+ private boolean mIsAccurateDelayCallbackEnabled;
private boolean mIsShortHostnamesEnabled;
private FlagOverrideProvider mOverrideProvider;
@@ -299,6 +318,7 @@
mAvoidAdvertisingEmptyTxtRecords = true; // Default enabled.
mIsCachedServicesRemovalEnabled = false;
mCachedServicesRetentionTime = DEFAULT_CACHED_SERVICES_RETENTION_TIME_MILLISECONDS;
+ mIsAccurateDelayCallbackEnabled = false;
mIsShortHostnamesEnabled = true; // Default enabled.
mOverrideProvider = null;
}
@@ -426,6 +446,16 @@
}
/**
+ * Set whether the accurate delay callback is enabled.
+ *
+ * @see #NSD_ACCURATE_DELAY_CALLBACK
+ */
+ public Builder setIsAccurateDelayCallbackEnabled(boolean isAccurateDelayCallbackEnabled) {
+ mIsAccurateDelayCallbackEnabled = isAccurateDelayCallbackEnabled;
+ return this;
+ }
+
+ /**
* Set whether the short hostnames feature is enabled.
*
* @see #NSD_USE_SHORT_HOSTNAMES
@@ -450,6 +480,7 @@
mAvoidAdvertisingEmptyTxtRecords,
mIsCachedServicesRemovalEnabled,
mCachedServicesRetentionTime,
+ mIsAccurateDelayCallbackEnabled,
mIsShortHostnamesEnabled,
mOverrideProvider);
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
index 8c86fb8..56d4b9a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -20,6 +20,7 @@
import static com.android.server.connectivity.mdns.MdnsSearchOptions.AGGRESSIVE_QUERY_MODE;
import static com.android.server.connectivity.mdns.MdnsServiceCache.ServiceExpiredCallback;
import static com.android.server.connectivity.mdns.MdnsServiceCache.findMatchedResponse;
+import static com.android.server.connectivity.mdns.MdnsQueryScheduler.ScheduledQueryTaskArgs;
import static com.android.server.connectivity.mdns.util.MdnsUtils.Clock;
import static com.android.server.connectivity.mdns.util.MdnsUtils.buildMdnsServiceInfoFromResponse;
@@ -36,6 +37,7 @@
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.DnsUtils;
+import com.android.net.module.util.RealtimeScheduler;
import com.android.net.module.util.SharedLog;
import com.android.server.connectivity.mdns.util.MdnsUtils;
@@ -94,6 +96,9 @@
private final boolean removeServiceAfterTtlExpires =
MdnsConfigs.removeServiceAfterTtlExpires();
private final Clock clock;
+ // Use RealtimeScheduler for query scheduling, which allows for more accurate sending of
+ // queries.
+ @Nullable private final RealtimeScheduler realtimeScheduler;
@Nullable private MdnsSearchOptions searchOptions;
@@ -139,8 +144,7 @@
public void handleMessage(Message msg) {
switch (msg.what) {
case EVENT_START_QUERYTASK: {
- final MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs =
- (MdnsQueryScheduler.ScheduledQueryTaskArgs) msg.obj;
+ final ScheduledQueryTaskArgs taskArgs = (ScheduledQueryTaskArgs) msg.obj;
// QueryTask should be run immediately after being created (not be scheduled in
// advance). Because the result of "makeResponsesForResolve" depends on answers
// that were received before it is called, so to take into account all answers
@@ -174,7 +178,7 @@
final long now = clock.elapsedRealtime();
lastSentTime = now;
final long minRemainingTtl = getMinRemainingTtl(now);
- MdnsQueryScheduler.ScheduledQueryTaskArgs args =
+ final ScheduledQueryTaskArgs args =
mdnsQueryScheduler.scheduleNextRun(
sentResult.taskArgs.config,
minRemainingTtl,
@@ -189,10 +193,14 @@
sharedLog.log(String.format("Query sent with transactionId: %d. "
+ "Next run: sessionId: %d, in %d ms",
sentResult.transactionId, args.sessionId, timeToNextTaskMs));
- dependencies.sendMessageDelayed(
- handler,
- handler.obtainMessage(EVENT_START_QUERYTASK, args),
- timeToNextTaskMs);
+ if (realtimeScheduler != null) {
+ setDelayedTask(args, timeToNextTaskMs);
+ } else {
+ dependencies.sendMessageDelayed(
+ handler,
+ handler.obtainMessage(EVENT_START_QUERYTASK, args),
+ timeToNextTaskMs);
+ }
break;
}
default:
@@ -254,6 +262,14 @@
return List.of(new DatagramPacket(queryBuffer, 0, queryBuffer.length, address));
}
}
+
+ /**
+ * @see RealtimeScheduler
+ */
+ @Nullable
+ public RealtimeScheduler createRealtimeScheduler(@NonNull Handler handler) {
+ return new RealtimeScheduler(handler);
+ }
}
/**
@@ -301,6 +317,8 @@
this.mdnsQueryScheduler = new MdnsQueryScheduler();
this.cacheKey = new MdnsServiceCache.CacheKey(serviceType, socketKey);
this.featureFlags = featureFlags;
+ this.realtimeScheduler = featureFlags.isAccurateDelayCallbackEnabled()
+ ? dependencies.createRealtimeScheduler(handler) : null;
}
/**
@@ -310,6 +328,9 @@
removeScheduledTask();
mdnsQueryScheduler.cancelScheduledRun();
serviceCache.unregisterServiceExpiredCallback(cacheKey);
+ if (realtimeScheduler != null) {
+ realtimeScheduler.close();
+ }
}
private List<MdnsResponse> getExistingServices() {
@@ -317,6 +338,12 @@
? serviceCache.getCachedServices(cacheKey) : Collections.emptyList();
}
+ private void setDelayedTask(ScheduledQueryTaskArgs args, long timeToNextTaskMs) {
+ realtimeScheduler.removeDelayedMessage(EVENT_START_QUERYTASK);
+ realtimeScheduler.sendDelayedMessage(
+ handler.obtainMessage(EVENT_START_QUERYTASK, args), timeToNextTaskMs);
+ }
+
/**
* Registers {@code listener} for receiving discovery event of mDNS service instances, and
* starts
@@ -363,7 +390,7 @@
}
final long minRemainingTtl = getMinRemainingTtl(now);
if (hadReply) {
- MdnsQueryScheduler.ScheduledQueryTaskArgs args =
+ final ScheduledQueryTaskArgs args =
mdnsQueryScheduler.scheduleNextRun(
taskConfig,
minRemainingTtl,
@@ -377,10 +404,14 @@
final long timeToNextTaskMs = calculateTimeToNextTask(args, now);
sharedLog.log(String.format("Schedule a query. Next run: sessionId: %d, in %d ms",
args.sessionId, timeToNextTaskMs));
- dependencies.sendMessageDelayed(
- handler,
- handler.obtainMessage(EVENT_START_QUERYTASK, args),
- timeToNextTaskMs);
+ if (realtimeScheduler != null) {
+ setDelayedTask(args, timeToNextTaskMs);
+ } else {
+ dependencies.sendMessageDelayed(
+ handler,
+ handler.obtainMessage(EVENT_START_QUERYTASK, args),
+ timeToNextTaskMs);
+ }
} else {
final List<MdnsResponse> servicesToResolve = makeResponsesForResolve(socketKey);
final QueryTask queryTask = new QueryTask(
@@ -420,7 +451,11 @@
}
private void removeScheduledTask() {
- dependencies.removeMessages(handler, EVENT_START_QUERYTASK);
+ if (realtimeScheduler != null) {
+ realtimeScheduler.removeDelayedMessage(EVENT_START_QUERYTASK);
+ } else {
+ dependencies.removeMessages(handler, EVENT_START_QUERYTASK);
+ }
sharedLog.log("Remove EVENT_START_QUERYTASK"
+ ", current session: " + currentSessionId);
++currentSessionId;
@@ -506,10 +541,13 @@
}
}
}
- if (dependencies.hasMessages(handler, EVENT_START_QUERYTASK)) {
+ final boolean hasScheduledTask = realtimeScheduler != null
+ ? realtimeScheduler.hasDelayedMessage(EVENT_START_QUERYTASK)
+ : dependencies.hasMessages(handler, EVENT_START_QUERYTASK);
+ if (hasScheduledTask) {
final long now = clock.elapsedRealtime();
final long minRemainingTtl = getMinRemainingTtl(now);
- MdnsQueryScheduler.ScheduledQueryTaskArgs args =
+ final ScheduledQueryTaskArgs args =
mdnsQueryScheduler.maybeRescheduleCurrentRun(now, minRemainingTtl,
lastSentTime, currentSessionId + 1,
searchOptions.numOfQueriesBeforeBackoff());
@@ -518,10 +556,14 @@
final long timeToNextTaskMs = calculateTimeToNextTask(args, now);
sharedLog.log(String.format("Reschedule a query. Next run: sessionId: %d, in %d ms",
args.sessionId, timeToNextTaskMs));
- dependencies.sendMessageDelayed(
- handler,
- handler.obtainMessage(EVENT_START_QUERYTASK, args),
- timeToNextTaskMs);
+ if (realtimeScheduler != null) {
+ setDelayedTask(args, timeToNextTaskMs);
+ } else {
+ dependencies.sendMessageDelayed(
+ handler,
+ handler.obtainMessage(EVENT_START_QUERYTASK, args),
+ timeToNextTaskMs);
+ }
}
}
}
@@ -686,10 +728,10 @@
private static class QuerySentArguments {
private final int transactionId;
private final List<String> subTypes = new ArrayList<>();
- private final MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs;
+ private final ScheduledQueryTaskArgs taskArgs;
QuerySentArguments(int transactionId, @NonNull List<String> subTypes,
- @NonNull MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs) {
+ @NonNull ScheduledQueryTaskArgs taskArgs) {
this.transactionId = transactionId;
this.subTypes.addAll(subTypes);
this.taskArgs = taskArgs;
@@ -698,14 +740,14 @@
// A FutureTask that enqueues a single query, and schedule a new FutureTask for the next task.
private class QueryTask implements Runnable {
- private final MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs;
+ private final ScheduledQueryTaskArgs taskArgs;
private final List<MdnsResponse> servicesToResolve = new ArrayList<>();
private final List<String> subtypes = new ArrayList<>();
private final boolean sendDiscoveryQueries;
private final List<MdnsResponse> existingServices = new ArrayList<>();
private final boolean onlyUseIpv6OnIpv6OnlyNetworks;
private final SocketKey socketKey;
- QueryTask(@NonNull MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs,
+ QueryTask(@NonNull ScheduledQueryTaskArgs taskArgs,
@NonNull Collection<MdnsResponse> servicesToResolve,
@NonNull Collection<String> subtypes, boolean sendDiscoveryQueries,
@NonNull Collection<MdnsResponse> existingServices,
@@ -771,7 +813,7 @@
return minRemainingTtl == Long.MAX_VALUE ? 0 : minRemainingTtl;
}
- private static long calculateTimeToNextTask(MdnsQueryScheduler.ScheduledQueryTaskArgs args,
+ private static long calculateTimeToNextTask(ScheduledQueryTaskArgs args,
long now) {
return Math.max(args.timeToRun - now, 0);
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java
index 1212e29..d91bd11 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java
@@ -564,7 +564,6 @@
// Never try mDNS on cellular, or on interfaces with incompatible flags
if (CollectionUtils.contains(transports, TRANSPORT_CELLULAR)
|| iface.isLoopback()
- || iface.isPointToPoint()
|| iface.isVirtual()
|| !iface.isUp()) {
return false;
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index eedf427..9b3c7ba 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -397,6 +397,16 @@
return mFactory.hasInterface(iface);
}
+ private List<String> getAllInterfaces() {
+ final ArrayList<String> interfaces = new ArrayList<>(
+ List.of(mFactory.getAvailableInterfaces(/* includeRestricted */ true)));
+
+ if (mTetheringInterfaceMode == INTERFACE_MODE_SERVER && mTetheringInterface != null) {
+ interfaces.add(mTetheringInterface);
+ }
+ return interfaces;
+ }
+
String[] getClientModeInterfaces(boolean includeRestricted) {
return mFactory.getAvailableInterfaces(includeRestricted);
}
@@ -459,10 +469,16 @@
public void setIncludeTestInterfaces(boolean include) {
mHandler.post(() -> {
mIncludeTestInterfaces = include;
- if (!include) {
+ if (include) {
+ trackAvailableInterfaces();
+ } else {
removeTestData();
+ // remove all test interfaces
+ for (String iface : getAllInterfaces()) {
+ if (isValidEthernetInterface(iface)) continue;
+ stopTrackingInterface(iface);
+ }
}
- trackAvailableInterfaces();
});
}
@@ -952,17 +968,7 @@
if (mIsEthernetEnabled == enabled) return;
mIsEthernetEnabled = enabled;
-
- // Interface in server mode should also be included.
- ArrayList<String> interfaces =
- new ArrayList<>(
- List.of(mFactory.getAvailableInterfaces(/* includeRestricted */ true)));
-
- if (mTetheringInterfaceMode == INTERFACE_MODE_SERVER) {
- interfaces.add(mTetheringInterface);
- }
-
- for (String iface : interfaces) {
+ for (String iface : getAllInterfaces()) {
setInterfaceUpState(iface, enabled);
}
broadcastEthernetStateChange(mIsEthernetEnabled);
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/Android.bp b/service/Android.bp
index c4e2ef0..8b469e4 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -161,6 +161,7 @@
],
libs: [
"framework-annotations-lib",
+ "framework-bluetooth.stubs.module_lib",
"framework-configinfrastructure.stubs.module_lib",
"framework-connectivity-pre-jarjar",
// The framework-connectivity-t library is only available on T+ platforms
diff --git a/service/ServiceConnectivityResources/OWNERS b/service/ServiceConnectivityResources/OWNERS
index df41ff2..c3c08ee 100644
--- a/service/ServiceConnectivityResources/OWNERS
+++ b/service/ServiceConnectivityResources/OWNERS
@@ -1,2 +1,3 @@
+per-file res/raw/ct_public_keys.pem = file:platform/packages/modules/Connectivity:main:/networksecurity/OWNERS
per-file res/values/config_thread.xml = file:platform/packages/modules/Connectivity:main:/thread/OWNERS
per-file res/values/overlayable.xml = file:platform/packages/modules/Connectivity:main:/thread/OWNERS
diff --git a/service/ServiceConnectivityResources/res/raw/ct_public_keys.pem b/service/ServiceConnectivityResources/res/raw/ct_public_keys.pem
new file mode 100644
index 0000000..8a5ebbf
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/raw/ct_public_keys.pem
@@ -0,0 +1,56 @@
+-----BEGIN PUBLIC KEY-----
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAmDwwE2FRpVJlw58fo5Ra
+Fsocb7DP3FJwwuaghXL3xPtyZisDDXIpfVG+UwDPyIGrRuYzeu9pjZ/0xGSYSPZ0
+l/H8L2XurInoAbj+Z370HB7W3njIOqG9rw5N6u/xT4nscBj1HKeUwh+Hwc0F1UHS
+MP8J32nWAfVepHrte3Jy+w/V7BId6WjJmxtI9OoJ7WTsoTeD+jLANUJWtpbx0p1L
+OAy70BlHbB0UvAJdMH149qi7Y9KaJ74Ea2ofKY43NWGgWfR+fY6V7CCfUXCOgvNM
+qq5QGyMnFKrlP0XkoOaVJkK92VEtyNff8KUXik2ZyUzhNkg4ZplCrhESWeykckB/
+mdZpVc45KZ+6Sx3U+FF30eRwlu2Nw2h1KKHzYfa6M1bcy1f/xw+IDq4R+1rR7sPb
+J2mMKz0OPeCXwGEXWzBuMOs0IQu6gyNdyVZcRSyQ+LcUzvEwksLP6G/ycqmwVfdw
+JE28k3MPUR3IxnMDQrdcZb7M7kjBoykKW3jQfwlEoK4EcNQbMXVn8Ws8rcwgQcQJ
+MjjQnbISojsJYo2fG+TE6d9rORB6CYVzICOj4YguXm4LO89cYQlR600W32pP5y3o
+3/yAd9OjsKrNfREDlcCXUx1APc7gOF351RFdHlDI0+RF/pIHbH3sww3VMCJ+tjst
+ZldgJk9yaz0cvOdKyVWC83ECAwEAAQ==
+-----END PUBLIC KEY-----
+-----BEGIN PUBLIC KEY-----
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnmb1lacOnP5H1bwb06mG
+fEUeC9PZRwNQskSs9KaWrpfrSkLKuHXkVCbgeagbUR/Sh1OeIhyJRSS0PLCO0JjC
+UpGhYMrIGRgEET4IrP9f8aMFqxxxBUEanI+OxAhIJlP9tiWfGdKAASYcxg/DyXXz
+bqSXBEFJqBLqmnfHcLB/NhO1ejV6AiU1NMrT+iWSrJG8b6mq/LlAqWvidK8oPBsW
+87M4pPLpUoA54ultjx2wEzJ9dBy6jtnKZ/dz4DkDhYug5izRDcYtEfzQBoum0etV
+s4EoogW1AMeqW5G+L4HjPNgp3gNGZ/2RaBy7gp8Br+byYu2wHwdQIBQjS310yaKc
+nuNFOd+Q0DrzvHKB7yYzzdwo+hNocPpkvOzSw74jd09kDZQ+S2peCJ5NPU7VKT6/
+tkvc3tYA9pAu4/T+BGqRft4FjgeNANfSIX/WhWDzzVWymTUGFUvt+D0fF3Cw+XSa
+b8uTgRZ1Ma/FvSGgXHVoG9E4QAFFG4I9mmRqsnzA+8fqSNAfieL5OWecq4PU+pMa
+uNVJ9hbvmW2yXuMgEg6K9kFLdxggRn+OcxowgXJJh79L0r7RN1d8kuHelDhOzcte
+dUTtLNOb/1PA0d/I2IVJwc9xSQZXurqqT/Z+c01B3/R0BgGDkIT/yZ5iHPoZFYPD
+U8UdQzUK0KXgGkc8P5pJW8kCAwEAAQ==
+-----END PUBLIC KEY-----
+-----BEGIN PUBLIC KEY-----
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAn1ssK1m56VVurVJ/fNKe
++aXDnytBy/hWY48ZT+ZC0S29llfjkaCBlmkngoI2hgwz9BI6pHwUINS15pT1sznw
+kHoEaNKr8sANHTQ0PYlDuk5iQSjnaGz7XmqbZ3c92BQrmLG/kwX2c17YNJI2qCk3
+MaBJBeS5YErR3xcucT9M7qtNWWIT9O0sLV1lDUZVCYedWBSnNz1/mAiLhttWmU5u
+GKl/5LmjWP/piNjh8whx0abJUGeGS2HH0JAOb0pjBV6UQvj3tO+gTiNDhdrE4CKh
+Qn8SKNjW/BY320f0A5t581Q0++cQUAisRgBQos0Pkvg5vb6wgII+pJq0SnZoYFfH
+oycuR4q3eOCgJmpEAPC0MhNpIDUQS6p3QabD9ID+21ymiQa/Zf8Mv2xMM6ZItKxX
+77vSKvBbimTGmB2IU+Zi484PKI5QwxBUCHVSmNpvHyXyjhBmpqik9Op26QYYT10b
+ADnJY1L/Q+i44nI4pfwgIncqAWuLnxg/XggJDWj48Un9SMNoyN7gzQX75M7rh7/t
+F6QtKvJreP0pP2UoVSgZKjXnL9tqeZbOdZU1kBHi1HOhlUKTfq5dn2fVUeYkE769
+clFF7Y1FiI259IPhTKiOIfARJ4BL4Sn8D9c9vpxDYPFl5bCJbspmFpwfzTMDnGVS
+/IlY6Putpv2/lD1B7aQGt1sCAwEAAQ==
+-----END PUBLIC KEY-----
+-----BEGIN PUBLIC KEY-----
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwMPNecDhLDamK9Rs8W5V
+c0LWZPbk9FNP+budBoFMX46dWyJ75O5e8xY9UhJSBFl8+Itm/rWO/h2zZ6qE3Cq8
+1LYbOg+x+rLnYOcAvD2O7EU73A3RD/vqoUDDVK3cwKMq3ry7CYu+NJW7TRIKV7Ct
+BMCBrvpmC1WlZ/jxSV0Soapza4/H+UV0hHYh/Wn3EWObGWYdI3yxwZ81AyU1QCR/
+oaO0PQPXqvo3gPBINnK3Qr0aLtYc4YCfTXe6i4g3DeAlkpqNLZtC2hyqiRB4Dg47
+zDzYECGofRAu8w9d8uI+eccacXfvI9zEcL7FAl5AzBKmMFOfBNTr08V7+aROWfGO
+7imWsj2MQ6RG17zqJak5QX/1bqDxwhG0KolB86mPZu0WeKz1B3iP5qAUlDNBHLDV
+pQIez0mrMsVsimVguuLYHMpIgijphA9WhijCJW2x7c6aocB6IpnMIV1sqnUQTwLG
+t32AMrckxqFmaKGj/8I9M+Xj+Cy4fIa5YSOdb/tlaYZSfjH5ch41xucQ2HWFyZ/9
+hkTFodvF5ajCQ5maHeIjDkS/Bc/s9CB+/fbSkstDsPMRp/ExyQcEYjKTG5o9Ewyo
++KGGXS2dSS10Ibl0Zx/S/0ZuZx8ZAxMOIIPpugdkWqHU9thh71dR8zM4KMkEfB8C
+sWLGB1yMuztn9nRUcpiEZTECAwEAAQ==
+-----END PUBLIC KEY-----
diff --git a/service/ServiceConnectivityResources/res/values/config.xml b/service/ServiceConnectivityResources/res/values/config.xml
index 2d3647a..1b0f29d 100644
--- a/service/ServiceConnectivityResources/res/values/config.xml
+++ b/service/ServiceConnectivityResources/res/values/config.xml
@@ -135,6 +135,15 @@
<string-array translatable="false" name="config_nsdOffloadServicesPriority">
</string-array>
+ <!-- An array of service types that shouldn't be offloaded via NsdManager#registerOffloadEngine.
+ Format is [service type], for example: "_testservice._tcp"
+ Due to limited RAM in hardware offload, we prioritize user-impacting services.
+ _googlezone._tcp, an internal Googlecast service, was therefore blocked.
+ -->
+ <string-array name="config_nsdOffloadServicesDenyList" translatable="false">
+ <item>_googlezone._tcp</item>
+ </string-array>
+
<!-- Whether to use an ongoing notification for signing in to captive portals, instead of a
notification that can be dismissed. -->
<bool name="config_ongoingSignInNotification">false</bool>
diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml
index 7ac86aa..5c0ba78 100644
--- a/service/ServiceConnectivityResources/res/values/overlayable.xml
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -31,6 +31,7 @@
<item type="integer" name="config_networkWakeupPacketMask"/>
<item type="integer" name="config_networkNotifySwitchType"/>
<item type="array" name="config_networkNotifySwitches"/>
+ <item type="array" name="config_nsdOffloadServicesDenyList"/>
<item type="array" name="config_nsdOffloadServicesPriority"/>
<item type="bool" name="config_ongoingSignInNotification"/>
<item type="bool" name="config_autoCancelNetworkNotifications"/>
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 44868b2d..36c0cf9 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -25,6 +25,8 @@
import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_MAP_PATH;
import static android.net.BpfNetMapsConstants.IIF_MATCH;
import static android.net.BpfNetMapsConstants.INGRESS_DISCARD_MAP_PATH;
+import static android.net.BpfNetMapsConstants.LOCAL_NET_ACCESS_MAP_PATH;
+import static android.net.BpfNetMapsConstants.LOCAL_NET_BLOCKED_UID_MAP_PATH;
import static android.net.BpfNetMapsConstants.LOCKDOWN_VPN_MATCH;
import static android.net.BpfNetMapsConstants.UID_OWNER_MAP_PATH;
import static android.net.BpfNetMapsConstants.UID_PERMISSION_MAP_PATH;
@@ -36,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;
@@ -74,6 +76,7 @@
import com.android.net.module.util.IBpfMap;
import com.android.net.module.util.SingleWriterBpfMap;
import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Bool;
import com.android.net.module.util.Struct.S32;
import com.android.net.module.util.Struct.U32;
import com.android.net.module.util.Struct.U8;
@@ -81,6 +84,7 @@
import com.android.net.module.util.bpf.CookieTagMapValue;
import com.android.net.module.util.bpf.IngressDiscardKey;
import com.android.net.module.util.bpf.IngressDiscardValue;
+import com.android.net.module.util.bpf.LocalNetAccessKey;
import java.io.FileDescriptor;
import java.io.IOException;
@@ -131,9 +135,12 @@
private static IBpfMap<S32, U8> sDataSaverEnabledMap = null;
private static IBpfMap<IngressDiscardKey, IngressDiscardValue> sIngressDiscardMap = null;
+ private static IBpfMap<LocalNetAccessKey, Bool> sLocalNetAccessMap = null;
+ 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")
);
/**
@@ -186,6 +193,25 @@
sIngressDiscardMap = ingressDiscardMap;
}
+ /**
+ * Set localNetAccessMap for test.
+ */
+ @VisibleForTesting
+ public static void setLocalNetAccessMapForTest(
+ IBpfMap<LocalNetAccessKey, Bool> localNetAccessMap) {
+ sLocalNetAccessMap = localNetAccessMap;
+ }
+
+ /**
+ * Set localNetBlockedUidMap for test.
+ */
+ @VisibleForTesting
+ public static void setLocalNetBlockedUidMapForTest(
+ IBpfMap<U32, Bool> localNetBlockedUidMap) {
+ sLocalNetBlockedUidMap = localNetBlockedUidMap;
+ }
+
+
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private static IBpfMap<S32, U32> getConfigurationMap() {
try {
@@ -247,6 +273,26 @@
}
}
+ @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+ private static IBpfMap<U32, Bool> getLocalNetBlockedUidMap() {
+ try {
+ return SingleWriterBpfMap.getSingleton(LOCAL_NET_BLOCKED_UID_MAP_PATH,
+ U32.class, Bool.class);
+ } catch (ErrnoException e) {
+ throw new IllegalStateException("Cannot open local_net_blocked_uid map", e);
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+ private static IBpfMap<LocalNetAccessKey, Bool> getLocalNetAccessMap() {
+ try {
+ return SingleWriterBpfMap.getSingleton(LOCAL_NET_ACCESS_MAP_PATH,
+ LocalNetAccessKey.class, Bool.class);
+ } catch (ErrnoException e) {
+ throw new IllegalStateException("Cannot open local_net_access map", e);
+ }
+ }
+
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private static void initBpfMaps() {
if (sConfigurationMap == null) {
@@ -299,6 +345,27 @@
} catch (ErrnoException e) {
throw new IllegalStateException("Failed to initialize ingress discard map", e);
}
+
+ if (isAtLeast25Q2()) {
+ if (sLocalNetAccessMap == null) {
+ sLocalNetAccessMap = getLocalNetAccessMap();
+ }
+ try {
+ sLocalNetAccessMap.clear();
+ } catch (ErrnoException e) {
+ throw new IllegalStateException("Failed to initialize local_net_access map", e);
+ }
+
+ if (sLocalNetBlockedUidMap == null) {
+ sLocalNetBlockedUidMap = getLocalNetBlockedUidMap();
+ }
+ try {
+ sLocalNetBlockedUidMap.clear();
+ } catch (ErrnoException e) {
+ throw new IllegalStateException("Failed to initialize local_net_blocked_uid map",
+ e);
+ }
+ }
}
/**
@@ -387,6 +454,21 @@
}
}
+ private void throwIfPre25Q2(final String msg) {
+ if (!isAtLeast25Q2()) {
+ throw new UnsupportedOperationException(msg);
+ }
+ }
+
+ /*
+ ToDo : Remove this method when SdkLevel.isAtLeastB() is fixed, aosp is at sdk level 36 or use
+ NetworkStackUtils.isAtLeast25Q2 when it is moved to a static lib.
+ */
+ public static boolean isAtLeast25Q2() {
+ return SdkLevel.isAtLeastB() || (SdkLevel.isAtLeastV()
+ && "Baklava".equals(Build.VERSION.CODENAME));
+ }
+
private void removeRule(final int uid, final long match, final String caller) {
try {
synchronized (sUidOwnerMap) {
@@ -788,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));
@@ -810,6 +893,158 @@
}
/**
+ * Add configuration to local_net_access trie map.
+ * @param lpmBitlen prefix length that will be used for longest matching
+ * @param iface interface name
+ * @param address remote address. ipv4 addresses would be mapped to v6
+ * @param protocol required for longest match in special cases
+ * @param remotePort src/dst port for ingress/egress
+ * @param isAllowed is the local network call allowed or blocked.
+ */
+ @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+ public void addLocalNetAccess(final int lpmBitlen, final String iface,
+ final InetAddress address, final int protocol, final int remotePort,
+ final boolean isAllowed) {
+ throwIfPre25Q2("addLocalNetAccess is not available on pre-B devices");
+ final int ifIndex;
+ if (iface == null) {
+ ifIndex = 0;
+ } else {
+ ifIndex = mDeps.getIfIndex(iface);
+ }
+ if (ifIndex == 0) {
+ Log.e(TAG, "Failed to get if index, skip addLocalNetAccess for " + address
+ + "(" + iface + ")");
+ return;
+ }
+ final LocalNetAccessKey localNetAccessKey = new LocalNetAccessKey(lpmBitlen, ifIndex,
+ address, protocol, remotePort);
+
+ try {
+ sLocalNetAccessMap.updateEntry(localNetAccessKey, new Bool(isAllowed));
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to add local network access for localNetAccessKey : "
+ + localNetAccessKey + ", isAllowed : " + isAllowed);
+ }
+ }
+
+ /**
+ * Remove configuration to local_net_access trie map.
+ * @param lpmBitlen prefix length that will be used for longest matching
+ * @param iface interface name
+ * @param address remote address. ipv4 addresses would be mapped to v6
+ * @param protocol required for longest match in special cases
+ * @param remotePort src/dst port for ingress/egress
+ */
+ @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+ public void removeLocalNetAccess(final int lpmBitlen, final String iface,
+ final InetAddress address, final int protocol, final int remotePort) {
+ throwIfPre25Q2("removeLocalNetAccess is not available on pre-B devices");
+ final int ifIndex;
+ if (iface == null) {
+ ifIndex = 0;
+ } else {
+ ifIndex = mDeps.getIfIndex(iface);
+ }
+ if (ifIndex == 0) {
+ Log.e(TAG, "Failed to get if index, skip removeLocalNetAccess for " + address
+ + "(" + iface + ")");
+ return;
+ }
+ final LocalNetAccessKey localNetAccessKey = new LocalNetAccessKey(lpmBitlen, ifIndex,
+ address, protocol, remotePort);
+
+ try {
+ sLocalNetAccessMap.deleteEntry(localNetAccessKey);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to remove local network access for localNetAccessKey : "
+ + localNetAccessKey);
+ }
+ }
+
+ /**
+ * Fetches value available in local_net_access bpf map for provided configuration
+ * @param lpmBitlen prefix length that will be used for longest matching
+ * @param iface interface name
+ * @param address remote address. ipv4 addresses would be mapped to v6
+ * @param protocol required for longest match in special cases
+ * @param remotePort src/dst port for ingress/egress
+ * @return false if the configuration is disallowed, true if the configuration is absent i.e. it
+ * is not local network or if configuration is allowed like local dns servers.
+ */
+ @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+ public boolean getLocalNetAccess(final int lpmBitlen, final String iface,
+ final InetAddress address, final int protocol, final int remotePort) {
+ throwIfPre25Q2("getLocalNetAccess is not available on pre-B devices");
+ final int ifIndex;
+ if (iface == null) {
+ ifIndex = 0;
+ } else {
+ ifIndex = mDeps.getIfIndex(iface);
+ }
+ if (ifIndex == 0) {
+ Log.e(TAG, "Failed to get if index, returning default from getLocalNetAccess for "
+ + address + "(" + iface + ")");
+ return true;
+ }
+ final LocalNetAccessKey localNetAccessKey = new LocalNetAccessKey(lpmBitlen, ifIndex,
+ address, protocol, remotePort);
+ try {
+ final Bool value = sLocalNetAccessMap.getValue(localNetAccessKey);
+ return value == null ? true : value.val;
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to find local network access configuration for "
+ + "localNetAccessKey : " + localNetAccessKey);
+ }
+ return true;
+ }
+
+ /**
+ * Add uid to local_net_blocked_uid map.
+ * @param uid application uid that needs to block local network calls.
+ */
+ @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+ public void addUidToLocalNetBlockMap(final int uid) {
+ throwIfPre25Q2("addUidToLocalNetBlockMap is not available on pre-B devices");
+ try {
+ sLocalNetBlockedUidMap.updateEntry(new U32(uid), new Bool(true));
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to add local network blocked for uid : " + uid);
+ }
+ }
+
+ /**
+ * True if local network calls are blocked for application.
+ * @param uid application uid that needs check if local network calls are blocked.
+ */
+ @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+ public boolean isUidBlockedFromUsingLocalNetwork(final int uid) {
+ throwIfPre25Q2("isUidBlockedFromUsingLocalNetwork is not available on pre-B devices");
+ try {
+ final Bool value = sLocalNetBlockedUidMap.getValue(new U32(uid));
+ return value == null ? false : value.val;
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to find uid(" + uid
+ + ") is present in local network blocked map");
+ }
+ return false;
+ }
+
+ /**
+ * Remove uid from local_net_blocked_uid map(if present).
+ * @param uid application uid that needs check if local network calls are blocked.
+ */
+ @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+ public void removeUidFromLocalNetBlockMap(final int uid) {
+ throwIfPre25Q2("removeUidFromLocalNetBlockMap is not available on pre-B devices");
+ try {
+ sLocalNetBlockedUidMap.deleteEntry(new U32(uid));
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to remove uid(" + uid + ") from local network blocked map");
+ }
+ }
+
+ /**
* Get granted permissions for specified uid. If uid is not in the map, this method returns
* {@link android.net.INetd.PERMISSION_INTERNET} since this is a default permission.
* See {@link #setNetPermForUids}
@@ -824,10 +1059,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;
}
}
@@ -976,7 +1211,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!";
}
@@ -1079,6 +1314,14 @@
(key, value) -> "[" + key.dstAddr + "]: "
+ value.iif1 + "(" + mDeps.getIfName(value.iif1) + "), "
+ value.iif2 + "(" + mDeps.getIfName(value.iif2) + ")");
+ if (sLocalNetBlockedUidMap != null) {
+ BpfDump.dumpMap(sLocalNetAccessMap, pw, "sLocalNetAccessMap",
+ (key, value) -> "[" + key + "]: " + value);
+ }
+ if (sLocalNetBlockedUidMap != null) {
+ BpfDump.dumpMap(sLocalNetBlockedUidMap, pw, "sLocalNetBlockedUidMap",
+ (key, value) -> "[" + key + "]: " + value);
+ }
dumpDataSaverConfig(pw);
pw.decreaseIndent();
}
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index afa69fe..7f827b8 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -109,8 +109,8 @@
import static android.net.NetworkCapabilities.REDACT_FOR_ACCESS_FINE_LOCATION;
import static android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS;
import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS;
-import static android.net.NetworkCapabilities.RES_ID_UNSET;
import static android.net.NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS;
+import static android.net.NetworkCapabilities.RES_ID_UNSET;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
@@ -146,6 +146,8 @@
import static com.android.net.module.util.BpfUtils.BPF_CGROUP_UDP6_RECVMSG;
import static com.android.net.module.util.BpfUtils.BPF_CGROUP_UDP6_SENDMSG;
import static com.android.net.module.util.NetworkMonitorUtils.isPrivateDnsValidationRequired;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_LOCAL_PREFIXES;
+import static com.android.net.module.util.NetworkStackConstants.MULTICAST_AND_BROADCAST_PREFIXES;
import static com.android.net.module.util.PermissionUtils.enforceAnyPermissionOf;
import static com.android.net.module.util.PermissionUtils.enforceNetworkStackPermission;
import static com.android.net.module.util.PermissionUtils.enforceNetworkStackPermissionOr;
@@ -214,6 +216,7 @@
import android.net.InetAddresses;
import android.net.IpMemoryStore;
import android.net.IpPrefix;
+import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.LocalNetworkConfig;
import android.net.LocalNetworkInfo;
@@ -420,14 +423,14 @@
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
/**
* @hide
*/
-public class ConnectivityService extends IConnectivityManager.Stub
- implements PendingIntent.OnFinished {
+public class ConnectivityService extends IConnectivityManager.Stub {
private static final String TAG = ConnectivityService.class.getSimpleName();
private static final String DIAG_ARG = "--diag";
@@ -561,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;
@@ -1020,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;
@@ -1620,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,
@@ -1798,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");
@@ -1876,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());
@@ -1899,11 +1913,12 @@
&& mDeps.isFeatureEnabled(context, REQUEST_RESTRICTED_WIFI);
mBackgroundFirewallChainEnabled = mDeps.isAtLeastV() && mDeps.isFeatureNotChickenedOut(
context, ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN);
- mUseDeclaredMethodsForCallbacksEnabled = mDeps.isFeatureEnabled(context,
- ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS);
+ mUseDeclaredMethodsForCallbacksEnabled =
+ mDeps.isFeatureNotChickenedOut(context,
+ ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS);
// registerUidFrozenStateChangedCallback is only available on U+
mQueueCallbacksForFrozenApps = mDeps.isAtLeastU()
- && mDeps.isFeatureEnabled(context, QUEUE_CALLBACKS_FOR_FROZEN_APPS);
+ && mDeps.isFeatureNotChickenedOut(context, QUEUE_CALLBACKS_FOR_FROZEN_APPS);
mCarrierPrivilegeAuthenticator = mDeps.makeCarrierPrivilegeAuthenticator(
mContext, mTelephonyManager, mRequestRestrictedWifiEnabled,
this::handleUidCarrierPrivilegesLost, mHandler);
@@ -2091,6 +2106,8 @@
}
mIngressToVpnAddressFiltering = mDeps.isAtLeastT()
&& mDeps.isFeatureNotChickenedOut(mContext, INGRESS_TO_VPN_ADDRESS_FILTERING);
+
+ mL2capNetworkProvider = mDeps.makeL2capNetworkProvider(mContext);
}
/**
@@ -4126,6 +4143,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);
@@ -5683,6 +5704,7 @@
// destroyed pending replacement they will be sent when it is disconnected.
maybeDisableForwardRulesForDisconnectingNai(nai, false /* sendCallbacks */);
updateIngressToVpnAddressFiltering(null, nai.linkProperties, nai);
+ updateLocalNetworkAddresses(null, nai.linkProperties);
try {
mNetd.networkDestroy(nai.network.getNetId());
} catch (RemoteException | ServiceSpecificException e) {
@@ -9583,6 +9605,8 @@
updateIngressToVpnAddressFiltering(newLp, oldLp, networkAgent);
+ updateLocalNetworkAddresses(newLp, oldLp);
+
updateMtu(newLp, oldLp);
// TODO - figure out what to do for clat
// for (LinkProperties lp : newLp.getStackedLinks()) {
@@ -9762,6 +9786,219 @@
}
/**
+ * Update Local Network Addresses to LocalNetAccess BPF map.
+ * @param newLp new link properties
+ * @param oldLp old link properties
+ */
+ private void updateLocalNetworkAddresses(@Nullable final LinkProperties newLp,
+ @NonNull final LinkProperties oldLp) {
+
+ // The maps are available only after 25Q2 release
+ if (!BpfNetMaps.isAtLeast25Q2()) {
+ return;
+ }
+
+ final CompareResult<String> interfaceDiff = new CompareResult<>(
+ oldLp != null ? oldLp.getAllInterfaceNames() : null,
+ newLp != null ? newLp.getAllInterfaceNames() : null);
+
+ for (final String iface : interfaceDiff.added) {
+ addLocalAddressesToBpfMap(iface, MULTICAST_AND_BROADCAST_PREFIXES, newLp);
+ }
+ for (final String iface : interfaceDiff.removed) {
+ removeLocalAddressesFromBpfMap(iface, MULTICAST_AND_BROADCAST_PREFIXES, oldLp);
+ }
+
+ // The both list contain current link properties + stacked links for new and old LP.
+ final List<LinkProperties> newLinkProperties = new ArrayList<>();
+ final List<LinkProperties> oldLinkProperties = new ArrayList<>();
+
+ if (newLp != null) {
+ 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
+ final Map<String, List<IpPrefix>> prefixesAddedForInterface = new ArrayMap<>();
+
+ final CompareResult<LinkProperties> linkPropertiesDiff = new CompareResult<>(
+ oldLinkProperties, newLinkProperties);
+
+ for (LinkProperties linkProperty : linkPropertiesDiff.added) {
+ final List<IpPrefix> unicastLocalPrefixesToBeAdded = new ArrayList<>();
+ for (LinkAddress linkAddress : linkProperty.getLinkAddresses()) {
+ unicastLocalPrefixesToBeAdded.addAll(
+ getLocalNetworkPrefixesForAddress(linkAddress));
+ }
+ addLocalAddressesToBpfMap(linkProperty.getInterfaceName(),
+ unicastLocalPrefixesToBeAdded, linkProperty);
+
+ // populating interface name -> ip prefixes which were added to local_net_access map.
+ if (!prefixesAddedForInterface.containsKey(linkProperty.getInterfaceName())) {
+ prefixesAddedForInterface.put(linkProperty.getInterfaceName(), new ArrayList<>());
+ }
+ prefixesAddedForInterface.get(linkProperty.getInterfaceName())
+ .addAll(unicastLocalPrefixesToBeAdded);
+ }
+
+ for (LinkProperties linkProperty : linkPropertiesDiff.removed) {
+ final List<IpPrefix> unicastLocalPrefixesToBeRemoved = new ArrayList<>();
+ final List<IpPrefix> unicastLocalPrefixesAdded = prefixesAddedForInterface.getOrDefault(
+ linkProperty.getInterfaceName(), Collections.emptyList());
+
+ 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 10.0.0.0/8 should not be
+ // removed due to removal of 10.0.11.0/24
+ unicastLocalPrefixesToBeRemoved.removeAll(unicastLocalPrefixesAdded);
+
+ removeLocalAddressesFromBpfMap(linkProperty.getInterfaceName(),
+ new ArrayList<>(unicastLocalPrefixesToBeRemoved), linkProperty);
+ }
+ }
+
+ /**
+ * Filters IpPrefix that are local prefixes and LinkAddress is part of them.
+ * @param linkAddress link address used for filtering
+ * @return list of IpPrefix that are local addresses.
+ */
+ private List<IpPrefix> getLocalNetworkPrefixesForAddress(LinkAddress linkAddress) {
+ List<IpPrefix> localPrefixes = new ArrayList<>();
+ if (linkAddress.isIpv6()) {
+ // For IPv6, if the prefix length is greater than zero then they are part of local
+ // network
+ if (linkAddress.getPrefixLength() != 0) {
+ localPrefixes.add(
+ new IpPrefix(linkAddress.getAddress(), linkAddress.getPrefixLength()));
+ }
+ } else {
+ // For IPv4, if the linkAddress is part of IpPrefix adding prefix to result.
+ for (IpPrefix ipv4LocalPrefix : IPV4_LOCAL_PREFIXES) {
+ if (ipv4LocalPrefix.containsPrefix(
+ new IpPrefix(linkAddress.getAddress(), linkAddress.getPrefixLength()))) {
+ localPrefixes.add(ipv4LocalPrefix);
+ }
+ }
+ }
+ return localPrefixes;
+ }
+
+ /**
+ * Adds list of prefixes(addresses) to local network access map.
+ * @param iface interface name
+ * @param prefixes list of prefixes/addresses
+ * @param lp LinkProperties
+ */
+ private void addLocalAddressesToBpfMap(final String iface, final List<IpPrefix> prefixes,
+ @Nullable final LinkProperties lp) {
+ if (!BpfNetMaps.isAtLeast25Q2()) return;
+
+ for (IpPrefix prefix : prefixes) {
+ // Add local dnses allow rule To BpfMap before adding the block rule for prefix
+ addLocalDnsesToBpfMap(iface, prefix, lp);
+ /*
+ Prefix length is used by LPM trie map(local_net_access_map) for performing longest
+ prefix matching, this length represents the maximum number of bits used for matching.
+ The interface index should always be matched which is 32-bit integer. For IPv6, prefix
+ length is calculated by adding the ip address prefix length along with interface index
+ making it (32 + length). IPv4 addresses are stored as ipv4-mapped-ipv6 which implies
+ first 96 bits are common for all ipv4 addresses. Hence, prefix length is calculated as
+ 32(interface index) + 96 (common for ipv4-mapped-ipv6) + length.
+ */
+ final int prefixLengthConstant = (prefix.isIPv4() ? (32 + 96) : 32);
+ mBpfNetMaps.addLocalNetAccess(prefixLengthConstant + prefix.getPrefixLength(),
+ iface, prefix.getAddress(), 0, 0, false);
+
+ }
+
+ }
+
+ /**
+ * Removes list of prefixes(addresses) from local network access map.
+ * @param iface interface name
+ * @param prefixes list of prefixes/addresses
+ * @param lp LinkProperties
+ */
+ private void removeLocalAddressesFromBpfMap(final String iface, final List<IpPrefix> prefixes,
+ @Nullable final LinkProperties lp) {
+ if (!BpfNetMaps.isAtLeast25Q2()) return;
+
+ for (IpPrefix prefix : prefixes) {
+ // The reasoning for prefix length is explained in addLocalAddressesToBpfMap()
+ final int prefixLengthConstant = (prefix.isIPv4() ? (32 + 96) : 32);
+ mBpfNetMaps.removeLocalNetAccess(prefixLengthConstant
+ + prefix.getPrefixLength(), iface, prefix.getAddress(), 0, 0);
+
+ // Also remove the allow rule for dnses included in the prefix after removing the block
+ // rule for prefix.
+ removeLocalDnsesFromBpfMap(iface, prefix, lp);
+ }
+ }
+
+ /**
+ * Adds DNS servers to local network access map, if included in the interface prefix
+ * @param iface interface name
+ * @param prefix IpPrefix
+ * @param lp LinkProperties
+ */
+ private void addLocalDnsesToBpfMap(final String iface, IpPrefix prefix,
+ @Nullable final LinkProperties lp) {
+ if (!BpfNetMaps.isAtLeast25Q2() || lp == null) return;
+
+ for (InetAddress dnsServer : lp.getDnsServers()) {
+ // Adds dns allow rule to LocalNetAccessMap for both TCP and UDP protocol at port 53,
+ // if it is a local dns (ie. it falls in the local prefix range).
+ if (prefix.contains(dnsServer)) {
+ mBpfNetMaps.addLocalNetAccess(getIpv4MappedAddressBitLen(), iface, dnsServer,
+ IPPROTO_UDP, 53, true);
+ mBpfNetMaps.addLocalNetAccess(getIpv4MappedAddressBitLen(), iface, dnsServer,
+ IPPROTO_TCP, 53, true);
+ }
+ }
+ }
+
+ /**
+ * Removes DNS servers from local network access map, if included in the interface prefix
+ * @param iface interface name
+ * @param prefix IpPrefix
+ * @param lp LinkProperties
+ */
+ private void removeLocalDnsesFromBpfMap(final String iface, IpPrefix prefix,
+ @Nullable final LinkProperties lp) {
+ if (!BpfNetMaps.isAtLeast25Q2() || lp == null) return;
+
+ for (InetAddress dnsServer : lp.getDnsServers()) {
+ // Removes dns allow rule from LocalNetAccessMap for both TCP and UDP protocol
+ // at port 53, if it is a local dns (ie. it falls in the prefix range).
+ if (prefix.contains(dnsServer)) {
+ mBpfNetMaps.removeLocalNetAccess(getIpv4MappedAddressBitLen(), iface, dnsServer,
+ IPPROTO_UDP, 53);
+ mBpfNetMaps.removeLocalNetAccess(getIpv4MappedAddressBitLen(), iface, dnsServer,
+ IPPROTO_TCP, 53);
+ }
+ }
+ }
+
+ /**
+ * Returns total bit length of an Ipv4 mapped address.
+ */
+ private int getIpv4MappedAddressBitLen() {
+ final int ifaceLen = 32; // bit length of interface
+ final int inetAddressLen = 32 + 96; // length of ipv4 mapped addresses
+ final int portProtocolLen = 32; //16 for port + 16 for protocol;
+ return ifaceLen + inetAddressLen + portProtocolLen;
+ }
+
+ /**
* Have netd update routes from oldLp to newLp.
* @return true if routes changed between oldLp and newLp
*/
@@ -10699,10 +10936,42 @@
// else not handled
}
+ /**
+ * A small class to manage releasing a lock exactly once even if releaseLock is called
+ * multiple times. See b/390043283
+ * PendingIntent#send throws CanceledException in various cases. In some of them it will
+ * still call onSendFinished, in others it won't and the client can't know. This class
+ * keeps a ref to the wakelock that it releases exactly once, thanks to Atomics semantics.
+ */
+ private class WakeLockOnFinishedReceiver implements PendingIntent.OnFinished {
+ private final AtomicReference<PowerManager.WakeLock> mLock;
+ WakeLockOnFinishedReceiver(@NonNull final PowerManager.WakeLock lock) {
+ mLock = new AtomicReference<>(lock);
+ lock.acquire();
+ }
+
+ public void releaseLock() {
+ final PowerManager.WakeLock lock = mLock.getAndSet(null);
+ if (null != lock) lock.release();
+ }
+
+ @Override
+ public void onSendFinished(PendingIntent pendingIntent, Intent intent, int resultCode,
+ String resultData, Bundle resultExtras) {
+ if (DBG) log("Finished sending " + pendingIntent);
+ releaseLock();
+ releasePendingNetworkRequestWithDelay(pendingIntent);
+ }
+ }
+
// TODO(b/193460475): Remove when tooling supports SystemApi to public API.
@SuppressLint("NewApi")
private void sendIntent(PendingIntent pendingIntent, Intent intent) {
- mPendingIntentWakeLock.acquire();
+ // Since the receiver will take the lock exactly once and release it exactly once, it
+ // is safe to pass the same wakelock to all receivers and avoid creating a new lock
+ // every time.
+ final WakeLockOnFinishedReceiver receiver =
+ new WakeLockOnFinishedReceiver(mPendingIntentWakeLock);
try {
if (DBG) log("Sending " + pendingIntent);
final BroadcastOptions options = BroadcastOptions.makeBasic();
@@ -10711,25 +10980,14 @@
// utilizing the PendingIntent as a backdoor to do this.
options.setPendingIntentBackgroundActivityLaunchAllowed(false);
}
- pendingIntent.send(mContext, 0, intent, this /* onFinished */, null /* Handler */,
+ pendingIntent.send(mContext, 0, intent, receiver, null /* Handler */,
null /* requiredPermission */,
mDeps.isAtLeastT() ? options.toBundle() : null);
} catch (PendingIntent.CanceledException e) {
if (DBG) log(pendingIntent + " was not sent, it had been canceled.");
- mPendingIntentWakeLock.release();
+ receiver.releaseLock();
releasePendingNetworkRequest(pendingIntent);
}
- // ...otherwise, mPendingIntentWakeLock.release() gets called by onSendFinished()
- }
-
- @Override
- public void onSendFinished(PendingIntent pendingIntent, Intent intent, int resultCode,
- String resultData, Bundle resultExtras) {
- if (DBG) log("Finished sending " + pendingIntent);
- mPendingIntentWakeLock.release();
- // Release with a delay so the receiving client has an opportunity to put in its
- // own request.
- releasePendingNetworkRequestWithDelay(pendingIntent);
}
@Nullable
diff --git a/service/src/com/android/server/L2capNetworkProvider.java b/service/src/com/android/server/L2capNetworkProvider.java
index c5ec9ee..0352ad5 100644
--- a/service/src/com/android/server/L2capNetworkProvider.java
+++ b/service/src/com/android/server/L2capNetworkProvider.java
@@ -16,9 +16,9 @@
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;
import static android.net.L2capNetworkSpecifier.ROLE_SERVER;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED;
@@ -29,9 +29,16 @@
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;
import android.annotation.Nullable;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.BluetoothServerSocket;
+import android.bluetooth.BluetoothSocket;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
@@ -41,27 +48,58 @@
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;
+import android.util.ArraySet;
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.L2capNetwork.L2capIpClient;
+import com.android.server.net.L2capPacketForwarder;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
+import java.util.Set;
public class L2capNetworkProvider {
private static final String TAG = L2capNetworkProvider.class.getSimpleName();
+ private static final NetworkCapabilities COMMON_CAPABILITIES =
+ // TODO: add NET_CAPABILITY_NOT_RESTRICTED and check that getRequestorUid() has
+ // BLUETOOTH_CONNECT permission.
+ NetworkCapabilities.Builder.withoutDefaultCapabilities()
+ .addTransportType(TRANSPORT_BLUETOOTH)
+ .addCapability(NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED)
+ .addCapability(NET_CAPABILITY_NOT_CONGESTED)
+ .addCapability(NET_CAPABILITY_NOT_METERED)
+ .addCapability(NET_CAPABILITY_NOT_ROAMING)
+ .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .addCapability(NET_CAPABILITY_NOT_VPN)
+ .build();
private final Dependencies mDeps;
private final Context mContext;
private final HandlerThread mHandlerThread;
private final Handler mHandler;
private final NetworkProvider mProvider;
private final BlanketReservationOffer mBlanketOffer;
- private final Map<Integer, ReservedServerOffer> mReservedServerOffers = new ArrayMap<>();
+ private final Set<ReservedServerOffer> mReservedServerOffers = new ArraySet<>();
+ private final ClientOffer mClientOffer;
+ // 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";
+ private static int sTunIndex = 0;
/**
* The blanket reservation offer is used to create an L2CAP server network, i.e. a network
@@ -71,107 +109,248 @@
* requests that do not have a NetworkSpecifier set.
*/
private class BlanketReservationOffer implements NetworkOfferCallback {
- // TODO: ensure that once the incoming request is satisfied, the blanket offer does not get
- // unneeded. This means the blanket offer must always outscore the reserved offer. This
- // might require setting the blanket offer as setTransportPrimary().
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 {
+ // Below capabilities will match any reservation request with an L2capNetworkSpecifier
+ // that specifies ROLE_SERVER or without a NetworkSpecifier.
final L2capNetworkSpecifier l2capNetworkSpecifier = new L2capNetworkSpecifier.Builder()
.setRole(ROLE_SERVER)
.build();
- NetworkCapabilities caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
- .addTransportType(TRANSPORT_BLUETOOTH)
- // TODO: consider removing NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED.
- .addCapability(NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED)
- .addCapability(NET_CAPABILITY_NOT_CONGESTED)
- .addCapability(NET_CAPABILITY_NOT_METERED)
- .addCapability(NET_CAPABILITY_NOT_ROAMING)
- .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
- .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
- .addCapability(NET_CAPABILITY_NOT_VPN)
+ NetworkCapabilities caps = new NetworkCapabilities.Builder(COMMON_CAPABILITIES)
.setNetworkSpecifier(l2capNetworkSpecifier)
.build();
+ // TODO: add #setReservationId() to NetworkCapabilities.Builder
caps.setReservationId(RES_ID_MATCH_ALL_RESERVATIONS);
CAPABILITIES = caps;
}
- // 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;
-
- // The ROLE_SERVER offer can be satisfied by a ROLE_ANY request.
- if (l2capSpec.getRole() != ROLE_SERVER) return false;
-
- // HEADER_COMPRESSION_ANY is never valid in a request.
- if (l2capSpec.getHeaderCompression() == HEADER_COMPRESSION_ANY) return false;
-
- // remoteAddr must be null for ROLE_SERVER requests.
- if (l2capSpec.getRemoteAddress() != null) return false;
-
- // reservation must allocate a PSM, so only PSM_ANY can be passed.
- if (l2capSpec.getPsm() != PSM_ANY) return false;
-
- return true;
- }
-
@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 (!specifier.isValidServerReservationSpecifier()) {
+ Log.i(TAG, "Ignoring invalid reservation request: " + request);
return;
}
- final NetworkCapabilities reservationCaps = request.networkCapabilities;
- final ReservedServerOffer reservedOffer = new ReservedServerOffer(reservationCaps);
+ final ReservedServerOffer reservedOffer = createReservedServerOffer(request);
+ if (reservedOffer == null) {
+ // Something went wrong when creating the offer. Send onUnavailable() to the app.
+ Log.e(TAG, "Failed to create L2cap server offer");
+ mProvider.declareNetworkRequestUnfulfillable(request);
+ return;
+ }
final NetworkCapabilities reservedCaps = reservedOffer.getReservedCapabilities();
mProvider.registerNetworkOffer(SCORE, reservedCaps, mHandler::post, reservedOffer);
- mReservedServerOffers.put(request.requestId, reservedOffer);
+ mReservedServerOffers.add(reservedOffer);
+ }
+
+ @Nullable
+ private ReservedServerOffer createReservedServerOffer(NetworkRequest reservation) {
+ final BluetoothAdapter bluetoothAdapter = mBluetoothManager.getAdapter();
+ if (bluetoothAdapter == null) {
+ Log.w(TAG, "Failed to get BluetoothAdapter");
+ return null;
+ }
+ final BluetoothServerSocket serverSocket;
+ try {
+ serverSocket = bluetoothAdapter.listenUsingInsecureL2capChannel();
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to open BluetoothServerSocket");
+ return null;
+ }
+
+ // Create the reserved capabilities partially from the reservation itself (non-reserved
+ // parts of the L2capNetworkSpecifier), the COMMON_CAPABILITIES, and the reserved data
+ // (BLE L2CAP PSM from the BluetoothServerSocket).
+ final NetworkCapabilities reservationNc = reservation.networkCapabilities;
+ final L2capNetworkSpecifier reservationSpec =
+ (L2capNetworkSpecifier) reservationNc.getNetworkSpecifier();
+ // Note: the RemoteAddress is unspecified for server networks.
+ final L2capNetworkSpecifier reservedSpec = new L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(reservationSpec.getHeaderCompression())
+ .setPsm(serverSocket.getPsm())
+ .build();
+ NetworkCapabilities reservedNc =
+ new NetworkCapabilities.Builder(COMMON_CAPABILITIES)
+ .setNetworkSpecifier(reservedSpec)
+ .build();
+ reservedNc.setReservationId(reservationNc.getReservationId());
+ return new ReservedServerOffer(reservedNc, serverSocket);
+ }
+
+ @Nullable
+ private ReservedServerOffer getReservedOfferForRequest(NetworkRequest request) {
+ final int rId = request.networkCapabilities.getReservationId();
+ for (ReservedServerOffer offer : mReservedServerOffers) {
+ // Comparing by reservationId is more explicit then using canBeSatisfiedBy() or the
+ // requestId.
+ if (offer.getReservedCapabilities().getReservationId() != rId) continue;
+ return offer;
+ }
+ return null;
}
@Override
public void onNetworkUnneeded(NetworkRequest request) {
- if (!mReservedServerOffers.containsKey(request.requestId)) {
- return;
- }
+ final ReservedServerOffer reservedOffer = getReservedOfferForRequest(request);
+ if (reservedOffer == null) return;
- final ReservedServerOffer reservedOffer = mReservedServerOffers.get(request.requestId);
// Note that the reserved offer gets torn down when the reservation goes away, even if
- // there are lingering requests.
- reservedOffer.tearDown();
- mProvider.unregisterNetworkOffer(reservedOffer);
+ // there are active (non-reservation) requests for said offer.
+ destroyAndUnregisterReservedOffer(reservedOffer);
+ }
+ }
+
+ 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);
+
+ reservedOffer.tearDown();
+ mProvider.unregisterNetworkOffer(reservedOffer);
+ }
+
+ @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 = mDeps.createTunInterface(ifname);
+ if (tunFd == null) {
+ return null;
+ }
+
+ return L2capNetwork.create(
+ mHandler, mContext, mProvider, ifname, socket, tunFd, caps, mDeps, cb);
+ }
+
+ private static void closeBluetoothSocket(BluetoothSocket socket) {
+ try {
+ socket.close();
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to close BluetoothSocket", e);
}
}
private class ReservedServerOffer implements NetworkOfferCallback {
- private final boolean mUseHeaderCompression;
- private final int mPsm;
private final NetworkCapabilities mReservedCapabilities;
+ private final AcceptThread mAcceptThread;
+ // This set should almost always contain at most one network. This is because all L2CAP
+ // server networks created by the same reserved offer are indistinguishable from each other,
+ // so that ConnectivityService will tear down all but the first. However, temporarily, there
+ // can be more than one network.
+ private final Set<L2capNetwork> mL2capNetworks = new ArraySet<>();
- public ReservedServerOffer(NetworkCapabilities reservationCaps) {
- // getNetworkSpecifier() is guaranteed to return a non-null L2capNetworkSpecifier.
- final L2capNetworkSpecifier reservationSpec =
- (L2capNetworkSpecifier) reservationCaps.getNetworkSpecifier();
- mUseHeaderCompression =
- reservationSpec.getHeaderCompression() == HEADER_COMPRESSION_6LOWPAN;
+ private class AcceptThread extends Thread {
+ private static final int TIMEOUT_MS = 500;
+ private final BluetoothServerSocket mServerSocket;
- // TODO: open BluetoothServerSocket and allocate a PSM.
- mPsm = 0x80;
+ public AcceptThread(BluetoothServerSocket serverSocket) {
+ super("L2capNetworkProvider-AcceptThread");
+ mServerSocket = serverSocket;
+ }
- final L2capNetworkSpecifier reservedSpec = new L2capNetworkSpecifier.Builder()
- .setRole(ROLE_SERVER)
- .setHeaderCompression(reservationSpec.getHeaderCompression())
- .setPsm(mPsm)
- .build();
- mReservedCapabilities = new NetworkCapabilities.Builder(reservationCaps)
- .setNetworkSpecifier(reservedSpec)
- .build();
+ 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 (true) {
+ final BluetoothSocket connectedSocket;
+ try {
+ connectedSocket = mServerSocket.accept();
+ } catch (IOException e) {
+ // Note calling BluetoothServerSocket#close() also triggers an IOException
+ // which is indistinguishable from any other exceptional behavior.
+ // postDestroyAndUnregisterReservedOffer() is always safe to call as it
+ // first checks whether the offer still exists; so if the
+ // BluetoothServerSocket was closed (i.e. on tearDown()) this is a noop.
+ Log.w(TAG, "BluetoothServerSocket closed or #accept failed", e);
+ postDestroyAndUnregisterReservedOffer();
+ return; // stop running immediately on error
+ }
+ postCreateServerNetwork(connectedSocket);
+ }
+ }
+
+ public void tearDown() {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+ try {
+ // BluetoothServerSocket.close() is thread-safe.
+ mServerSocket.close();
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to close BluetoothServerSocket", e);
+ }
+ try {
+ join();
+ } catch (InterruptedException e) {
+ // join() interrupted during tearDown(). Do nothing.
+ }
+ }
+ }
+
+ private boolean createServerNetwork(BluetoothSocket socket) {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+ // It is possible the offer went away.
+ if (!mReservedServerOffers.contains(this)) return false;
+
+ if (!socket.isConnected()) {
+ Log.wtf(TAG, "BluetoothSocket must be connected");
+ return false;
+ }
+
+ final L2capNetwork network = createL2capNetwork(socket, mReservedCapabilities,
+ new L2capNetwork.ICallback() {
+ @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) {
+ Log.e(TAG, "Failed to create L2capNetwork");
+ return false;
+ }
+
+ mL2capNetworks.add(network);
+ return true;
+ }
+
+ public ReservedServerOffer(NetworkCapabilities reservedCapabilities,
+ BluetoothServerSocket serverSocket) {
+ mReservedCapabilities = reservedCapabilities;
+ mAcceptThread = new AcceptThread(serverSocket);
+ mAcceptThread.start();
}
public NetworkCapabilities getReservedCapabilities() {
@@ -180,52 +359,309 @@
@Override
public void onNetworkNeeded(NetworkRequest request) {
- // TODO: implement
+ // UNUSED: the lifetime of the reserved network is controlled by the blanket offer.
}
@Override
public void onNetworkUnneeded(NetworkRequest request) {
- // TODO: implement
+ // UNUSED: the lifetime of the reserved network is controlled by the blanket offer.
+ }
+
+ /** 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();
+ }
+ }
+ }
+
+ private class ClientOffer implements NetworkOfferCallback {
+ public static final NetworkScore SCORE = new NetworkScore.Builder().build();
+ public static final NetworkCapabilities CAPABILITIES;
+ static {
+ // Below capabilities will match any request with an L2capNetworkSpecifier
+ // that specifies ROLE_CLIENT or without a NetworkSpecifier.
+ final L2capNetworkSpecifier l2capNetworkSpecifier = new L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_CLIENT)
+ .build();
+ CAPABILITIES = new NetworkCapabilities.Builder(COMMON_CAPABILITIES)
+ .setNetworkSpecifier(l2capNetworkSpecifier)
+ .build();
+ }
+
+ private final Map<L2capNetworkSpecifier, ClientRequestInfo> mClientNetworkRequests =
+ new ArrayMap<>();
+
+ /**
+ * State object to store information for client NetworkRequests.
+ */
+ private static class ClientRequestInfo {
+ public final L2capNetworkSpecifier specifier;
+ public final List<NetworkRequest> requests = new ArrayList<>();
+ // TODO: add support for retries.
+ public final ConnectThread connectThread;
+ @Nullable
+ public L2capNetwork network;
+
+ public ClientRequestInfo(NetworkRequest request, ConnectThread connectThread) {
+ this.specifier = (L2capNetworkSpecifier) request.getNetworkSpecifier();
+ this.requests.add(request);
+ this.connectThread = connectThread;
+ }
+ }
+
+ // TODO: consider using ExecutorService
+ private class ConnectThread extends Thread {
+ private final L2capNetworkSpecifier mSpecifier;
+ private final BluetoothSocket mSocket;
+
+ public ConnectThread(L2capNetworkSpecifier specifier, BluetoothSocket socket) {
+ super("L2capNetworkProvider-ConnectThread");
+ mSpecifier = specifier;
+ mSocket = socket;
+ }
+
+ @Override
+ public void run() {
+ try {
+ mSocket.connect();
+ mHandler.post(() -> {
+ final boolean success = createClientNetwork(mSpecifier, mSocket);
+ if (!success) closeBluetoothSocket(mSocket);
+ });
+ } catch (IOException e) {
+ Log.w(TAG, "BluetoothSocket was closed or #connect failed", e);
+ // It is safe to call BluetoothSocket#close() multiple times.
+ closeBluetoothSocket(mSocket);
+ mHandler.post(() -> {
+ // Note that if the Socket was closed, this call is a noop as the
+ // ClientNetworkRequest has already been removed.
+ declareAllNetworkRequestsUnfulfillable(mSpecifier);
+ });
+ }
+ }
+
+ public void abort() {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+ // Closing the BluetoothSocket is the only way to unblock connect() because it calls
+ // shutdown on the underlying (connected) SOCK_SEQPACKET.
+ // It is safe to call BluetoothSocket#close() multiple times.
+ closeBluetoothSocket(mSocket);
+ try {
+ join();
+ } catch (InterruptedException e) {
+ Log.i(TAG, "Interrupted while joining ConnectThread", e);
+ }
+ }
+ }
+
+ 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;
+
+ final NetworkCapabilities caps = new NetworkCapabilities.Builder(CAPABILITIES)
+ .setNetworkSpecifier(specifier)
+ .build();
+
+ 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) {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+ declareAllNetworkRequestsUnfulfillable(specifier);
+ }
+ @Override
+ public void onNetworkUnwanted(L2capNetwork network) {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+ declareAllNetworkRequestsUnfulfillable(specifier);
+ }
+ });
+ if (network == null) return false;
+
+ cri.network = network;
+ return true;
+ }
+
+ @Override
+ public void onNetworkNeeded(NetworkRequest 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 (!requestSpecifier.isValidClientRequestSpecifier()) {
+ Log.i(TAG, "Ignoring invalid client request: " + request);
+ return;
+ }
+
+ // Check whether this exact request is already being tracked.
+ final ClientRequestInfo cri = mClientNetworkRequests.get(requestSpecifier);
+ if (cri != null) {
+ Log.d(TAG, "The request is already being tracked. NetworkRequest: " + request);
+ cri.requests.add(request);
+ return;
+ }
+
+ // Check whether a fuzzy match shows a mismatch in header compression by calling
+ // canBeSatisfiedBy().
+ // TODO: Add a copy constructor to L2capNetworkSpecifier.Builder.
+ final L2capNetworkSpecifier matchAnyHeaderCompressionSpecifier =
+ new L2capNetworkSpecifier.Builder()
+ .setRole(requestSpecifier.getRole())
+ .setRemoteAddress(requestSpecifier.getRemoteAddress())
+ .setPsm(requestSpecifier.getPsm())
+ .setHeaderCompression(HEADER_COMPRESSION_ANY)
+ .build();
+ for (L2capNetworkSpecifier existingSpecifier : mClientNetworkRequests.keySet()) {
+ if (existingSpecifier.canBeSatisfiedBy(matchAnyHeaderCompressionSpecifier)) {
+ // This requeset can never be serviced as this network already exists with a
+ // different header compression mechanism.
+ mProvider.declareNetworkRequestUnfulfillable(request);
+ return;
+ }
+ }
+
+ // If the code reaches here, this is a new request.
+ final BluetoothAdapter bluetoothAdapter = mBluetoothManager.getAdapter();
+ if (bluetoothAdapter == null) {
+ Log.w(TAG, "Failed to get BluetoothAdapter");
+ mProvider.declareNetworkRequestUnfulfillable(request);
+ return;
+ }
+
+ final byte[] macAddress = requestSpecifier.getRemoteAddress().toByteArray();
+ final BluetoothDevice bluetoothDevice = bluetoothAdapter.getRemoteDevice(macAddress);
+ final BluetoothSocket socket;
+ try {
+ socket = bluetoothDevice.createInsecureL2capChannel(requestSpecifier.getPsm());
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to createInsecureL2capChannel", e);
+ mProvider.declareNetworkRequestUnfulfillable(request);
+ return;
+ }
+
+ final ConnectThread connectThread = new ConnectThread(requestSpecifier, socket);
+ connectThread.start();
+ final ClientRequestInfo newRequestInfo = new ClientRequestInfo(request, connectThread);
+ mClientNetworkRequests.put(requestSpecifier, newRequestInfo);
+ }
+
+ @Override
+ public void onNetworkUnneeded(NetworkRequest request) {
+ final L2capNetworkSpecifier specifier =
+ (L2capNetworkSpecifier) request.getNetworkSpecifier();
+
+ // Map#get() is safe to call with null key
+ final ClientRequestInfo cri = mClientNetworkRequests.get(specifier);
+ if (cri == null) return;
+
+ cri.requests.remove(request);
+ if (cri.requests.size() > 0) return;
+
+ // If the code reaches here, the network needs to be torn down.
+ releaseClientNetworkRequest(cri);
}
/**
- * Called when the reservation goes away and the reserved offer must be torn down.
+ * Release the client network request and tear down all associated state.
*
- * This method can be called multiple times.
+ * Only call this when all associated NetworkRequests have been released.
*/
- public void tearDown() {
- // TODO: implement.
- // This method can be called multiple times.
+ 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
+ // request will already be removed from mClientNetworkRequests by the time the
+ // createClientNetwork() call is processed on the handler, so it is safe to call
+ // #abort().
+ cri.connectThread.abort();
+ }
+
+ if (cri.network != null) {
+ cri.network.tearDown();
+ }
+ }
+
+ private void declareAllNetworkRequestsUnfulfillable(L2capNetworkSpecifier specifier) {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+ final ClientRequestInfo cri = mClientNetworkRequests.get(specifier);
+ if (cri == null) return;
+
+ for (NetworkRequest request : cri.requests) {
+ mProvider.declareNetworkRequestUnfulfillable(request);
+ }
+ releaseClientNetworkRequest(cri);
}
}
@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);
+ }
+
+ /** Create an L2capIpClient */
+ public L2capIpClient createL2capIpClient(String logTag, Context context, String ifname) {
+ return new L2capIpClient(logTag, context, ifname);
+ }
}
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();
}
/**
@@ -234,11 +670,22 @@
* Called on CS Handler thread.
*/
public void start() {
- final PackageManager pm = mContext.getPackageManager();
- if (pm.hasSystemFeature(FEATURE_BLUETOOTH_LE)) {
+ 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);
- }
+ mProvider.registerNetworkOffer(ClientOffer.SCORE,
+ ClientOffer.CAPABILITIES, mHandler::post, mClientOffer);
+ });
}
}
diff --git a/service/src/com/android/server/connectivity/MulticastRoutingCoordinatorService.java b/service/src/com/android/server/connectivity/MulticastRoutingCoordinatorService.java
index af4aee5..c4de80f 100644
--- a/service/src/com/android/server/connectivity/MulticastRoutingCoordinatorService.java
+++ b/service/src/com/android/server/connectivity/MulticastRoutingCoordinatorService.java
@@ -21,6 +21,7 @@
import static android.net.MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE;
import static android.system.OsConstants.AF_INET6;
import static android.system.OsConstants.EADDRINUSE;
+import static android.system.OsConstants.EADDRNOTAVAIL;
import static android.system.OsConstants.IPPROTO_ICMPV6;
import static android.system.OsConstants.IPPROTO_IPV6;
import static android.system.OsConstants.SOCK_CLOEXEC;
@@ -258,6 +259,10 @@
mDependencies.setsockoptMrt6DelMif(mMulticastRoutingFd, virtualIndex);
Log.d(TAG, "Removed mifi " + virtualIndex + " from MIF");
} catch (ErrnoException e) {
+ if (e.errno == EADDRNOTAVAIL) {
+ Log.w(TAG, "multicast virtual interface " + virtualIndex + " already removed", e);
+ return;
+ }
Log.e(TAG, "failed to remove multicast virtual interface" + virtualIndex, e);
}
}
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index 2686e4a..e762a8e 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -1128,6 +1128,7 @@
int delta = add ? +1 : -1;
switch (request.type) {
case REQUEST:
+ case RESERVATION:
mNumRequestNetworkRequests += delta;
break;
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 beaa174..5de5f61 100755
--- a/service/src/com/android/server/connectivity/PermissionMonitor.java
+++ b/service/src/com/android/server/connectivity/PermissionMonitor.java
@@ -19,25 +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;
@@ -60,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;
@@ -96,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;
@@ -227,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> {
@@ -267,18 +280,49 @@
}
@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;
mBpfNetMaps = bpfNetMaps;
mThread = thread;
+ if (BpfNetMaps.isAtLeast25Q2()) {
+ // Local net restrictions is supported as a developer opt-in starting in Android B.
+ // 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(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);
+ }
+ }
}
private void ensureRunningOnHandlerThread() {
@@ -341,6 +385,7 @@
uidsPerm.put(sdkSandboxUid, permission);
}
}
+ setLocalNetworkPermissions(uid, app.packageName);
}
return uidsPerm;
}
@@ -395,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);
@@ -403,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);
@@ -588,7 +633,7 @@
final List<PackageInfo> apps = getInstalledPackagesAsUser(user);
- // Save all apps
+ // Save all apps in mAllApps
updateAllApps(apps);
// Uids network permissions
@@ -625,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));
}
}
@@ -646,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);
@@ -698,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;
}
@@ -741,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";
@@ -766,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);
@@ -784,7 +834,7 @@
installed = true;
}
}
- return installed ? permission : PERMISSION_UNINSTALLED;
+ return installed ? permission : TRAFFIC_PERMISSION_UNINSTALLED;
}
/**
@@ -819,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
@@ -863,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);
@@ -921,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;
@@ -1164,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:
@@ -1188,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) {
@@ -1204,7 +1260,7 @@
toIntArray(noPermissionAppIds));
}
if (uninstalledAppIds.size() != 0) {
- mBpfNetMaps.setNetPermForUids(PERMISSION_UNINSTALLED,
+ mBpfNetMaps.setNetPermForUids(TRAFFIC_PERMISSION_UNINSTALLED,
toIntArray(uninstalledAppIds));
}
} catch (RemoteException | ServiceSpecificException e) {
@@ -1311,4 +1367,11 @@
private static void loge(String s, Throwable e) {
Log.e(TAG, s, e);
}
+
+ private class PermissionChangeListener implements PackageManager.OnPermissionsChangedListener {
+ @Override
+ public void onPermissionsChanged(int uid) {
+ 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
new file mode 100644
index 0000000..ca155db
--- /dev/null
+++ b/service/src/com/android/server/net/L2capNetwork.java
@@ -0,0 +1,177 @@
+/*
+ * 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 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;
+import android.net.NetworkCapabilities;
+import android.net.NetworkProvider;
+import android.net.NetworkScore;
+import android.net.ip.IIpClient;
+import android.net.ip.IpClientCallbacks;
+import android.net.ip.IpClientManager;
+import android.net.ip.IpClientUtil;
+import android.net.shared.ProvisioningConfiguration;
+import android.os.ConditionVariable;
+import android.os.Handler;
+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 L2capPacketForwarder mForwarder;
+ private final NetworkCapabilities mNetworkCapabilities;
+ private final NetworkAgent mNetworkAgent;
+
+ /** IpClient wrapper to handle IPv6 link-local provisioning for L2CAP tun.
+ *
+ * Note that the IpClient does not need to be stopped.
+ */
+ public static class L2capIpClient extends IpClientCallbacks {
+ private final String mLogTag;
+ private final ConditionVariable mOnIpClientCreatedCv = new ConditionVariable(false);
+ private final ConditionVariable mOnProvisioningSuccessCv = new ConditionVariable(false);
+ @Nullable
+ private IpClientManager mIpClient;
+ @Nullable
+ private volatile LinkProperties mLinkProperties;
+
+ public L2capIpClient(String logTag, Context context, String ifname) {
+ mLogTag = logTag;
+ IpClientUtil.makeIpClient(context, ifname, this);
+ }
+
+ @Override
+ public void onIpClientCreated(IIpClient ipClient) {
+ mIpClient = new IpClientManager(ipClient, mLogTag);
+ mOnIpClientCreatedCv.open();
+ }
+
+ @Override
+ public void onProvisioningSuccess(LinkProperties 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.
+ final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+ .withoutIPv4()
+ .withIpv6LinkLocalOnly()
+ .withRandomMacAddress() // addr_gen_mode EUI64 -> random on tun.
+ .build();
+ mIpClient.startProvisioning(config);
+ // "Provisioning" is guaranteed to succeed as link-local only mode does not actually
+ // require any provisioning.
+ mOnProvisioningSuccessCv.block();
+ return mLinkProperties;
+ }
+ }
+
+ public interface ICallback {
+ /** Called when an error is encountered */
+ void onError(L2capNetwork network);
+ /** Called when CS triggers NetworkAgent#onNetworkUnwanted */
+ void onNetworkUnwanted(L2capNetwork network);
+ }
+
+ 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;
+ 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);
+ });
+
+ final NetworkAgentConfig config = new NetworkAgentConfig.Builder().build();
+ mNetworkAgent = new NetworkAgent(context, mHandler.getLooper(), mLogTag,
+ nc, lp, NETWORK_SCORE, config, provider) {
+ @Override
+ public void onNetworkUnwanted() {
+ Log.i(mLogTag, "Network is unwanted");
+ // TODO: add a check that this callback is invoked on the handler thread.
+ cb.onNetworkUnwanted(L2capNetwork.this);
+ }
+ };
+ mNetworkAgent.register();
+ 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 = deps.createL2capIpClient(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;
+ }
+
+ /** Tear down the network and associated resources */
+ public void tearDown() {
+ mNetworkAgent.unregister();
+ mForwarder.tearDown();
+ }
+}
diff --git a/service/src/com/android/server/net/L2capPacketForwarder.java b/service/src/com/android/server/net/L2capPacketForwarder.java
new file mode 100644
index 0000000..8420d60
--- /dev/null
+++ b/service/src/com/android/server/net/L2capPacketForwarder.java
@@ -0,0 +1,341 @@
+/*
+ * 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 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;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+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.
+ *
+ * The forwarding logic operates on raw IP packets and there are no ethernet headers.
+ * Therefore, L3 MTU = L2 MTU.
+ */
+public class L2capPacketForwarder {
+ private static final String TAG = "L2capPacketForwarder";
+
+ // DCT specifies an MTU of 1500.
+ // TODO: Set /proc/sys/net/ipv6/conf/${iface}/mtu to 1280 and the link MTU to 1528 to accept
+ // slightly larger packets on ingress (i.e. packets passing through a NAT64 gateway).
+ // MTU determines the value of the read buffers, so use the larger of the two.
+ @VisibleForTesting
+ public static final int MTU = 1528;
+ private final Handler mHandler;
+ private final IReadWriteFd mTunFd;
+ private final IReadWriteFd mL2capFd;
+ private final L2capThread mIngressThread;
+ private final L2capThread mEgressThread;
+ private final ICallback mCallback;
+
+ public interface ICallback {
+ /** Called when an error is encountered; should tear down forwarding. */
+ void onError();
+ }
+
+ private interface IReadWriteFd {
+ /**
+ * Read up to len bytes into bytes[off] and return bytes actually read.
+ *
+ * bytes[] must be of size >= off + len.
+ */
+ int read(byte[] bytes, int off, int len) throws IOException;
+ /**
+ * Write len bytes starting from bytes[off]
+ *
+ * bytes[] must be of size >= off + len.
+ */
+ void write(byte[] bytes, int off, int len) throws IOException;
+ /** Disallow further receptions, shutdown(fd, SHUT_RD) */
+ void shutdownRead();
+ /** Disallow further transmissions, shutdown(fd, SHUT_WR) */
+ void shutdownWrite();
+ /** Close the fd */
+ void close();
+ }
+
+ @VisibleForTesting
+ public static class BluetoothSocketWrapper implements IReadWriteFd {
+ private final BluetoothSocket mSocket;
+ private final InputStream mInputStream;
+ private final OutputStream mOutputStream;
+
+ public BluetoothSocketWrapper(BluetoothSocket socket) {
+ // TODO: assert that MTU can fit within Bluetooth L2CAP SDU (maximum size of an L2CAP
+ // packet). The L2CAP SDU is 65535 by default, but can be less when using hardware
+ // offload.
+ mSocket = socket;
+ try {
+ mInputStream = socket.getInputStream();
+ mOutputStream = socket.getOutputStream();
+ } catch (IOException e) {
+ // Per the API docs, this should not actually be possible.
+ Log.wtf(TAG, "Failed to get Input/OutputStream", e);
+ // Fail hard.
+ throw new IllegalStateException("Failed to get Input/OutputStream");
+ }
+ }
+
+ /** Read from the BluetoothSocket. */
+ @Override
+ public int read(byte[] bytes, int off, int len) throws IOException {
+ // Note: EINTR is handled internally and automatically triggers a retry loop.
+ int bytesRead = mInputStream.read(bytes, off, len);
+ if (bytesRead < 0 || bytesRead > MTU) {
+ // Don't try to recover, just trigger network teardown. This might indicate a bug in
+ // the Bluetooth stack.
+ throw new IOException("Packet exceeds MTU or reached EOF. Read: " + bytesRead);
+ }
+ return bytesRead;
+ }
+
+ /** Write to the BluetoothSocket. */
+ @Override
+ public void write(byte[] bytes, int off, int len) throws IOException {
+ // Note: EINTR is handled internally and automatically triggers a retry loop.
+ mOutputStream.write(bytes, off, len);
+ }
+
+ @Override
+ public void shutdownRead() {
+ // BluetoothSocket does not expose methods to shutdown read / write individually;
+ // however, BluetoothSocket#close() shuts down both read and write and is safe to be
+ // called multiple times from any thread.
+ try {
+ mSocket.close();
+ } catch (IOException e) {
+ Log.w(TAG, "shutdownRead: Failed to close BluetoothSocket", e);
+ }
+ }
+
+ @Override
+ public void shutdownWrite() {
+ // BluetoothSocket does not expose methods to shutdown read / write individually;
+ // however, BluetoothSocket#close() shuts down both read and write and is safe to be
+ // called multiple times from any thread.
+ try {
+ mSocket.close();
+ } catch (IOException e) {
+ Log.w(TAG, "shutdownWrite: Failed to close BluetoothSocket", e);
+ }
+ }
+
+ @Override
+ public void close() {
+ // BluetoothSocket#close() is safe to be called multiple times.
+ try {
+ mSocket.close();
+ } catch (IOException e) {
+ Log.w(TAG, "close: Failed to close BluetoothSocket", e);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public static class FdWrapper implements IReadWriteFd {
+ private final ParcelFileDescriptor mFd;
+
+ public FdWrapper(ParcelFileDescriptor fd) {
+ mFd = fd;
+ }
+
+ @Override
+ public int read(byte[] bytes, int off, int len) throws IOException {
+ try {
+ // Note: EINTR is handled internally and automatically triggers a retry loop.
+ return Os.read(mFd.getFileDescriptor(), bytes, off, len);
+ } catch (ErrnoException e) {
+ throw new IOException(e);
+ }
+ }
+
+ /**
+ * Write to BluetoothSocket.
+ */
+ @Override
+ public void write(byte[] bytes, int off, int len) throws IOException {
+ try {
+ // Note: EINTR is handled internally and automatically triggers a retry loop.
+ Os.write(mFd.getFileDescriptor(), bytes, off, len);
+ } catch (ErrnoException e) {
+ throw new IOException(e);
+ }
+ }
+
+ @Override
+ public void shutdownRead() {
+ try {
+ Os.shutdown(mFd.getFileDescriptor(), OsConstants.SHUT_RD);
+ } catch (ErrnoException e) {
+ Log.w(TAG, "shutdownRead: Failed to shutdown(fd, SHUT_RD)", e);
+ }
+ }
+
+ @Override
+ public void shutdownWrite() {
+ try {
+ Os.shutdown(mFd.getFileDescriptor(), OsConstants.SHUT_WR);
+ } catch (ErrnoException e) {
+ Log.w(TAG, "shutdownWrite: Failed to shutdown(fd, SHUT_WR)", e);
+ }
+ }
+
+ @Override
+ public void close() {
+ try {
+ // Safe to call multiple times. Both Os.close(FileDescriptor) and
+ // ParcelFileDescriptor#close() offer protection against double-closing an fd.
+ mFd.close();
+ } catch (IOException e) {
+ Log.w(TAG, "close: Failed to close fd", e);
+ }
+ }
+ }
+
+ private class L2capThread extends Thread {
+ // Set mBuffer length to MTU + 1 to catch read() overflows.
+ private final byte[] mBuffer = new byte[MTU + 1];
+ private volatile boolean mIsRunning = true;
+
+ private final String mLogTag;
+ private final IReadWriteFd mReadFd;
+ private final IReadWriteFd mWriteFd;
+ private final boolean mIsIngress;
+ private final boolean mCompressHeaders;
+
+ 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() {
+ mHandler.post(() -> {
+ // All callbacks must be called on handler thread.
+ mCallback.onError();
+ });
+ }
+
+ @Override
+ public void run() {
+ while (mIsRunning) {
+ try {
+ 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 (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 <= 0 || readBytes > MTU) continue;
+
+ mWriteFd.write(mBuffer, 0 /*off*/, readBytes);
+ } catch (IOException|BufferUnderflowException e) {
+ Log.e(mLogTag, "L2capThread exception", e);
+ // Tear down the network on any error.
+ mIsRunning = false;
+ // Notify provider that forwarding has stopped.
+ postOnError();
+ }
+ }
+ }
+
+ public void tearDown() {
+ mIsRunning = false;
+ mReadFd.shutdownRead();
+ mWriteFd.shutdownWrite();
+ }
+ }
+
+ public L2capPacketForwarder(Handler handler, ParcelFileDescriptor tunFd, BluetoothSocket socket,
+ boolean compressHdrs, ICallback cb) {
+ this(handler, new FdWrapper(tunFd), new BluetoothSocketWrapper(socket), compressHdrs, cb);
+ }
+
+ @VisibleForTesting
+ L2capPacketForwarder(Handler handler, IReadWriteFd tunFd, IReadWriteFd l2capFd,
+ boolean compressHeaders, ICallback cb) {
+ mHandler = handler;
+ mTunFd = tunFd;
+ mL2capFd = l2capFd;
+ mCallback = cb;
+
+ mIngressThread = new L2capThread(l2capFd, tunFd, true /*isIngress*/, compressHeaders);
+ mEgressThread = new L2capThread(tunFd, l2capFd, false /*isIngress*/, compressHeaders);
+
+ mIngressThread.start();
+ mEgressThread.start();
+ }
+
+ /**
+ * Tear down the L2capPacketForwarder.
+ *
+ * This operation closes both the passed tun fd and BluetoothSocket.
+ **/
+ public void tearDown() {
+ // Destroying both threads first guarantees that both read and write side of FD have been
+ // shutdown.
+ mIngressThread.tearDown();
+ mEgressThread.tearDown();
+
+ // In order to interrupt a blocking read on the BluetoothSocket, the BluetoothSocket must be
+ // closed (which triggers shutdown()). This means, the BluetoothSocket must be closed inside
+ // L2capPacketForwarder. Tear down the tun fd alongside it for consistency.
+ mTunFd.close();
+ mL2capFd.close();
+
+ try {
+ mIngressThread.join();
+ } catch (InterruptedException e) {
+ // join() interrupted in tearDown path, do nothing.
+ }
+ try {
+ mEgressThread.join();
+ } catch (InterruptedException e) {
+ // join() interrupted in tearDown path, do nothing.
+ }
+ }
+}
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index 0eab6e7..8034e57 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -438,7 +438,11 @@
srcs: [
"device/com/android/net/module/util/FdEventsReader.java",
"device/com/android/net/module/util/HandlerUtils.java",
+ "device/com/android/net/module/util/JniUtil.java",
+ "device/com/android/net/module/util/RealtimeScheduler.java",
"device/com/android/net/module/util/SharedLog.java",
+ "device/com/android/net/module/util/ServiceConnectivityJni.java",
+ "device/com/android/net/module/util/TimerFdUtils.java",
"framework/com/android/net/module/util/ByteUtils.java",
"framework/com/android/net/module/util/CollectionUtils.java",
"framework/com/android/net/module/util/DnsUtils.java",
diff --git a/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java b/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
index 8b2fe58..4af516d 100644
--- a/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
+++ b/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
@@ -148,25 +148,6 @@
netd.tetherStartWithConfiguration(config);
}
- /** Setup interface for tethering. */
- public static void tetherInterface(final INetd netd, int netId, final String iface,
- final IpPrefix dest) throws RemoteException, ServiceSpecificException {
- tetherInterface(netd, netId, iface, dest, 20 /* maxAttempts */, 50 /* pollingIntervalMs */);
- }
-
- /** Setup interface with configurable retries for tethering. */
- public static void tetherInterface(final INetd netd, int netId, final String iface,
- final IpPrefix dest, int maxAttempts, int pollingIntervalMs)
- throws RemoteException, ServiceSpecificException {
- netd.tetherInterfaceAdd(iface);
- networkAddInterface(netd, netId, iface, maxAttempts, pollingIntervalMs);
- // Activate a route to dest and IPv6 link local.
- modifyRoute(netd, ModifyOperation.ADD, netId,
- new RouteInfo(dest, null, iface, RTN_UNICAST));
- modifyRoute(netd, ModifyOperation.ADD, netId,
- new RouteInfo(new IpPrefix("fe80::/64"), null, iface, RTN_UNICAST));
- }
-
/**
* Retry Netd#networkAddInterface for EBUSY error code.
* If the same interface (e.g., wlan0) is in client mode and then switches to tethered mode.
@@ -174,7 +155,7 @@
* in use in netd because the ConnectivityService thread hasn't processed the disconnect yet.
* See b/158269544 for detail.
*/
- private static void networkAddInterface(final INetd netd, int netId, final String iface,
+ public static void networkAddInterface(final INetd netd, int netId, final String iface,
int maxAttempts, int pollingIntervalMs)
throws ServiceSpecificException, RemoteException {
for (int i = 1; i <= maxAttempts; i++) {
@@ -193,16 +174,6 @@
}
}
- /** Reset interface for tethering. */
- public static void untetherInterface(final INetd netd, int netId, String iface)
- throws RemoteException, ServiceSpecificException {
- try {
- netd.tetherInterfaceRemove(iface);
- } finally {
- netd.networkRemoveInterface(netId, iface);
- }
- }
-
/** Add |routes| to the given network. */
public static void addRoutesToNetwork(final INetd netd, int netId, final String iface,
final List<RouteInfo> routes) {
diff --git a/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java b/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java
index c2fbb56..0241d0a 100644
--- a/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java
+++ b/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java
@@ -24,13 +24,9 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -39,7 +35,6 @@
import android.net.INetd;
import android.net.InterfaceConfigurationParcel;
-import android.net.IpPrefix;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
@@ -61,7 +56,6 @@
@Mock private INetd mNetd;
private static final String IFACE = "TEST_IFACE";
- private static final IpPrefix TEST_IPPREFIX = new IpPrefix("192.168.42.1/24");
private static final int TEST_NET_ID = 123;
@Before
@@ -150,20 +144,21 @@
}
@Test
- public void testTetherInterfaceSuccessful() throws Exception {
+ public void testNetworkAddInterfaceSuccessful() throws Exception {
// Expect #networkAddInterface successful at first tries.
- verifyTetherInterfaceSucceeds(1);
+ verifyNetworkAddInterfaceSucceeds(1);
// Expect #networkAddInterface successful after 10 tries.
- verifyTetherInterfaceSucceeds(10);
+ verifyNetworkAddInterfaceSucceeds(10);
}
- private void runTetherInterfaceWithServiceSpecificException(int expectedTries,
+ private void runNetworkAddInterfaceWithServiceSpecificException(int expectedTries,
int expectedCode) throws Exception {
setNetworkAddInterfaceOutcome(new ServiceSpecificException(expectedCode), expectedTries);
try {
- NetdUtils.tetherInterface(mNetd, TEST_NET_ID, IFACE, TEST_IPPREFIX, 20, 0);
+ NetdUtils.networkAddInterface(mNetd, TEST_NET_ID, IFACE,
+ 20 /* maxAttempts */, 0 /* pollingIntervalMs */);
fail("Expect throw ServiceSpecificException");
} catch (ServiceSpecificException e) {
assertEquals(e.errorCode, expectedCode);
@@ -173,11 +168,12 @@
reset(mNetd);
}
- private void runTetherInterfaceWithRemoteException(int expectedTries) throws Exception {
+ private void runNetworkAddInterfaceWithRemoteException(int expectedTries) throws Exception {
setNetworkAddInterfaceOutcome(new RemoteException(), expectedTries);
try {
- NetdUtils.tetherInterface(mNetd, TEST_NET_ID, IFACE, TEST_IPPREFIX, 20, 0);
+ NetdUtils.networkAddInterface(mNetd, TEST_NET_ID, IFACE,
+ 20 /* maxAttempts */, 0 /* pollingIntervalMs */);
fail("Expect throw RemoteException");
} catch (RemoteException e) { }
@@ -186,41 +182,37 @@
}
private void verifyNetworkAddInterfaceFails(int expectedTries) throws Exception {
- verify(mNetd).tetherInterfaceAdd(IFACE);
verify(mNetd, times(expectedTries)).networkAddInterface(TEST_NET_ID, IFACE);
- verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), any(), any());
-
verifyNoMoreInteractions(mNetd);
}
- private void verifyTetherInterfaceSucceeds(int expectedTries) throws Exception {
+ private void verifyNetworkAddInterfaceSucceeds(int expectedTries) throws Exception {
setNetworkAddInterfaceOutcome(null, expectedTries);
- NetdUtils.tetherInterface(mNetd, TEST_NET_ID, IFACE, TEST_IPPREFIX);
- verify(mNetd).tetherInterfaceAdd(IFACE);
+ NetdUtils.networkAddInterface(mNetd, TEST_NET_ID, IFACE,
+ 20 /* maxAttempts */, 0 /* pollingIntervalMs */);
verify(mNetd, times(expectedTries)).networkAddInterface(TEST_NET_ID, IFACE);
- verify(mNetd, times(2)).networkAddRoute(eq(TEST_NET_ID), eq(IFACE), any(), any());
verifyNoMoreInteractions(mNetd);
reset(mNetd);
}
@Test
- public void testTetherInterfaceFailOnNetworkAddInterface() throws Exception {
+ public void testFailOnNetworkAddInterface() throws Exception {
// Test throwing ServiceSpecificException with EBUSY failure.
- runTetherInterfaceWithServiceSpecificException(20, EBUSY);
+ runNetworkAddInterfaceWithServiceSpecificException(20, EBUSY);
// Test throwing ServiceSpecificException with unexpectedError.
final int unexpectedError = 999;
- runTetherInterfaceWithServiceSpecificException(1, unexpectedError);
+ runNetworkAddInterfaceWithServiceSpecificException(1, unexpectedError);
// Test throwing ServiceSpecificException with unexpectedError after 7 tries.
- runTetherInterfaceWithServiceSpecificException(7, unexpectedError);
+ runNetworkAddInterfaceWithServiceSpecificException(7, unexpectedError);
// Test throwing RemoteException.
- runTetherInterfaceWithRemoteException(1);
+ runNetworkAddInterfaceWithRemoteException(1);
// Test throwing RemoteException after 3 tries.
- runTetherInterfaceWithRemoteException(3);
+ runNetworkAddInterfaceWithRemoteException(3);
}
@Test
diff --git a/staticlibs/device/com/android/net/module/util/BpfDump.java b/staticlibs/device/com/android/net/module/util/BpfDump.java
index 4227194..d79f52e 100644
--- a/staticlibs/device/com/android/net/module/util/BpfDump.java
+++ b/staticlibs/device/com/android/net/module/util/BpfDump.java
@@ -23,6 +23,7 @@
import android.util.Pair;
import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
import java.io.PrintWriter;
import java.nio.ByteBuffer;
@@ -132,17 +133,35 @@
}
}
+ public static class Dependencies {
+ /**
+ * Call {@link Os#access}
+ */
+ public boolean access(String path, int mode) throws ErrnoException {
+ return Os.access(path, mode);
+ }
+ }
+
/**
* Dump the BpfMap status
*/
public static <K extends Struct, V extends Struct> void dumpMapStatus(IBpfMap<K, V> map,
PrintWriter pw, String mapName, String path) {
+ dumpMapStatus(map, pw, mapName, path, new Dependencies());
+ }
+
+ /**
+ * Dump the BpfMap status. Only test should use this method directly.
+ */
+ @VisibleForTesting
+ public static <K extends Struct, V extends Struct> void dumpMapStatus(IBpfMap<K, V> map,
+ PrintWriter pw, String mapName, String path, Dependencies deps) {
if (map != null) {
pw.println(mapName + ": OK");
return;
}
try {
- Os.access(path, R_OK);
+ deps.access(path, R_OK);
pw.println(mapName + ": NULL(map is pinned to " + path + ")");
} catch (ErrnoException e) {
pw.println(mapName + ": NULL(map is not pinned to " + path + ": "
diff --git a/staticlibs/device/com/android/net/module/util/PrivateAddressCoordinator.java b/staticlibs/device/com/android/net/module/util/PrivateAddressCoordinator.java
index bb95585..2ce5b86 100644
--- a/staticlibs/device/com/android/net/module/util/PrivateAddressCoordinator.java
+++ b/staticlibs/device/com/android/net/module/util/PrivateAddressCoordinator.java
@@ -33,12 +33,14 @@
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkCapabilities;
+import android.os.Build;
import android.os.RemoteException;
import android.util.ArrayMap;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
import java.io.PrintWriter;
import java.net.Inet4Address;
@@ -67,9 +69,6 @@
// WARNING: Keep in sync with chooseDownstreamAddress
public static final int PREFIX_LENGTH = 24;
- public static final String TETHER_FORCE_RANDOM_PREFIX_BASE_SELECTION =
- "tether_force_random_prefix_base_selection";
-
// Upstream monitor would be stopped when tethering is down. When tethering restart, downstream
// address may be requested before coordinator get current upstream notification. To ensure
// coordinator do not select conflict downstream prefix, mUpstreamPrefixMap would not be cleared
@@ -258,8 +257,15 @@
return null;
}
+ // TODO: Remove this method when SdkLevel.isAtLeastB() is fixed, aosp is at sdk level 36 or use
+ // NetworkStackUtils.isAtLeast25Q2 when it is moved to a static lib.
+ public static boolean isAtLeast25Q2() {
+ return SdkLevel.isAtLeastB() || (SdkLevel.isAtLeastV()
+ && "Baklava".equals(Build.VERSION.CODENAME));
+ }
+
private int getRandomPrefixIndex() {
- if (!mDeps.isFeatureEnabled(TETHER_FORCE_RANDOM_PREFIX_BASE_SELECTION)) return 0;
+ if (!isAtLeast25Q2()) return 0;
final int random = getRandomInt() & 0xffffff;
// This is to select the starting prefix range (/8, /12, or /16) instead of the actual
diff --git a/staticlibs/device/com/android/net/module/util/RealtimeScheduler.java b/staticlibs/device/com/android/net/module/util/RealtimeScheduler.java
index c8fdf72..2d95223 100644
--- a/staticlibs/device/com/android/net/module/util/RealtimeScheduler.java
+++ b/staticlibs/device/com/android/net/module/util/RealtimeScheduler.java
@@ -227,6 +227,13 @@
return enqueueTask(new MessageTask(msg, SystemClock.elapsedRealtime() + delayMs), delayMs);
}
+ private static boolean isMessageTask(Task task, int what) {
+ if (task instanceof MessageTask && ((MessageTask) task).mMessage.what == what) {
+ return true;
+ }
+ return false;
+ }
+
/**
* Remove a scheduled message.
*
@@ -234,8 +241,24 @@
*/
public void removeDelayedMessage(int what) {
ensureRunningOnCorrectThread();
- mTaskQueue.removeIf(task -> task instanceof MessageTask
- && ((MessageTask) task).mMessage.what == what);
+ mTaskQueue.removeIf(task -> isMessageTask(task, what));
+ }
+
+ /**
+ * Check if there is a scheduled message.
+ *
+ * @param what the message to be checked
+ * @return true if there is a target message, false otherwise.
+ */
+ public boolean hasDelayedMessage(int what) {
+ ensureRunningOnCorrectThread();
+
+ for (Task task : mTaskQueue) {
+ if (isMessageTask(task, what)) {
+ return true;
+ }
+ }
+ return false;
}
/**
diff --git a/staticlibs/device/com/android/net/module/util/ServiceConnectivityJni.java b/staticlibs/device/com/android/net/module/util/ServiceConnectivityJni.java
index 4a5dd4f..1d3561a 100644
--- a/staticlibs/device/com/android/net/module/util/ServiceConnectivityJni.java
+++ b/staticlibs/device/com/android/net/module/util/ServiceConnectivityJni.java
@@ -17,8 +17,7 @@
package com.android.net.module.util;
import android.annotation.NonNull;
-
-import java.io.IOException;
+import android.system.ErrnoException;
/**
* Contains JNI functions for use in service-connectivity
@@ -38,17 +37,17 @@
/**
* Create a timerfd.
*
- * @throws IOException if the timerfd creation is failed.
+ * @throws ErrnoException if the timerfd creation is failed.
*/
- public static native int createTimerFd() throws IOException;
+ public static native int createTimerFd() throws ErrnoException;
/**
* Set given time to the timerfd.
*
* @param timeMs target time
- * @throws IOException if setting expiration time is failed.
+ * @throws ErrnoException if setting expiration time is failed.
*/
- public static native void setTimerFdTime(int fd, long timeMs) throws IOException;
+ public static native void setTimerFdTime(int fd, long timeMs) throws ErrnoException;
/** Create tun/tap interface */
public static native int createTunTap(boolean isTun, boolean hasCarrier,
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/staticlibs/device/com/android/net/module/util/TimerFdUtils.java b/staticlibs/device/com/android/net/module/util/TimerFdUtils.java
index 10bc595..cce7efd 100644
--- a/staticlibs/device/com/android/net/module/util/TimerFdUtils.java
+++ b/staticlibs/device/com/android/net/module/util/TimerFdUtils.java
@@ -16,11 +16,9 @@
package com.android.net.module.util;
-import android.os.Process;
+import android.system.ErrnoException;
import android.util.Log;
-import java.io.IOException;
-
/**
* Contains mostly timerfd functionality.
*/
@@ -33,7 +31,7 @@
static int createTimerFileDescriptor() {
try {
return ServiceConnectivityJni.createTimerFd();
- } catch (IOException e) {
+ } catch (ErrnoException e) {
Log.e(TAG, "createTimerFd failed", e);
return -1;
}
@@ -45,7 +43,7 @@
static boolean setExpirationTime(int fd, long expirationTimeMs) {
try {
ServiceConnectivityJni.setTimerFdTime(fd, expirationTimeMs);
- } catch (IOException e) {
+ } catch (ErrnoException e) {
Log.e(TAG, "setExpirationTime failed", e);
return false;
}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java b/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
index fecaa09..c9a89ec 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
@@ -309,16 +309,18 @@
}
private static void sendNetlinkDestroyRequest(FileDescriptor fd, int proto,
- InetDiagMessage diagMsg) throws InterruptedIOException, ErrnoException {
+ StructInetDiagSockId id, short family, int state)
+ throws InterruptedIOException, ErrnoException {
+ // TODO: Investigate if it's fine to always set 0 to state and remove state from the arg
final byte[] destroyMsg = InetDiagMessage.inetDiagReqV2(
proto,
- diagMsg.inetDiagMsg.id,
- diagMsg.inetDiagMsg.idiag_family,
+ id,
+ family,
SOCK_DESTROY,
(short) (StructNlMsgHdr.NLM_F_REQUEST | StructNlMsgHdr.NLM_F_ACK),
0 /* pad */,
0 /* idiagExt */,
- 1 << diagMsg.inetDiagMsg.idiag_state
+ state
);
NetlinkUtils.sendMessage(fd, destroyMsg, 0, destroyMsg.length, IO_TIMEOUT_MS);
NetlinkUtils.receiveNetlinkAck(fd);
@@ -343,7 +345,8 @@
Consumer<InetDiagMessage> handleNlDumpMsg = (diagMsg) -> {
if (filter.test(diagMsg)) {
try {
- sendNetlinkDestroyRequest(destroyFd, proto, diagMsg);
+ sendNetlinkDestroyRequest(destroyFd, proto, diagMsg.inetDiagMsg.id,
+ diagMsg.inetDiagMsg.idiag_family, 1 << diagMsg.inetDiagMsg.idiag_state);
destroyedSockets.getAndIncrement();
} catch (InterruptedIOException | ErrnoException e) {
if (!(e instanceof ErrnoException
@@ -484,6 +487,30 @@
Log.d(TAG, "Destroyed live tcp sockets for uids=" + ownerUids + " in " + durationMs + "ms");
}
+ /**
+ * Close the udp socket which can be uniquely identified with the cookie and other information.
+ */
+ public static void destroyUdpSocket(final InetSocketAddress src, final InetSocketAddress dst,
+ final int ifIndex, final long cookie)
+ throws ErrnoException, SocketException, InterruptedIOException {
+ FileDescriptor fd = null;
+
+ try {
+ fd = NetlinkUtils.createNetLinkInetDiagSocket();
+ connectToKernel(fd);
+ final int family = (src.getAddress() instanceof Inet6Address) ? AF_INET6 : AF_INET;
+ final StructInetDiagSockId id = new StructInetDiagSockId(
+ src,
+ dst,
+ ifIndex,
+ cookie
+ );
+ sendNetlinkDestroyRequest(fd, IPPROTO_UDP, id, (short) family, 0 /* state */);
+ } finally {
+ closeSocketQuietly(fd);
+ }
+ }
+
@Override
public String toString() {
return "InetDiagMessage{ "
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
index e2544d3..2420e7a 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
@@ -83,7 +83,7 @@
public static final int INET_DIAG_INFO = 2;
public static final int INET_DIAG_MARK = 15;
- public static final long IO_TIMEOUT_MS = 300L;
+ public static final long IO_TIMEOUT_MS = 3000L;
public static final int DEFAULT_RECV_BUFSIZE = 8 * 1024;
public static final int SOCKET_RECV_BUFSIZE = 64 * 1024;
@@ -497,4 +497,33 @@
return false;
}
}
+
+ /**
+ * Sends a netlink request to set MTU for given interface
+ *
+ * @param interfaceName The name of the network interface to query.
+ * @param mtu MTU value to set for the interface.
+ * @return true if the request finished successfully, otherwise false.
+ */
+ public static boolean setInterfaceMtu(@NonNull String interfaceName, int mtu) {
+ if (mtu < 68) {
+ Log.e(TAG, "Invalid mtu: " + mtu + ", mtu should be greater than 68 referring RFC791");
+ return false;
+ }
+ final RtNetlinkLinkMessage ntMsg =
+ RtNetlinkLinkMessage.createSetMtuMessage(interfaceName, /*seqNo*/ 0, mtu);
+ if (ntMsg == null) {
+ Log.e(TAG, "Failed to create message to set MTU to " + mtu
+ + "for interface " + interfaceName);
+ return false;
+ }
+ final byte[] msg = ntMsg.pack(ByteOrder.nativeOrder());
+ try {
+ NetlinkUtils.sendOneShotKernelMessage(NETLINK_ROUTE, msg);
+ return true;
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to set MTU to " + mtu + " for: " + interfaceName, e);
+ return false;
+ }
+ }
}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
index 1afe3b8..c19a124 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
@@ -357,12 +357,44 @@
// interface, including change in administrative state. While RTM_SETLINK is used to
// modify an existing link rather than creating a new one.
return RtNetlinkLinkMessage.build(
- new StructNlMsgHdr(/*payloadLen*/ 0, RTM_NEWLINK, NLM_F_REQUEST, sequenceNumber),
+ new StructNlMsgHdr(
+ /*payloadLen*/ 0, RTM_NEWLINK, NLM_F_REQUEST_ACK, sequenceNumber),
new StructIfinfoMsg((short) AF_UNSPEC, /*type*/ 0, interfaceIndex,
flagsBits, changeBits),
DEFAULT_MTU, /*hardwareAddress*/ null, /*interfaceName*/ null);
}
+ /**
+ * Creates an {@link RtNetlinkLinkMessage} instance that can be used to set the MTU of a
+ * network interface.
+ *
+ * @param interfaceName The name of the network interface to query.
+ * @param sequenceNumber The sequence number for the Netlink message.
+ * @param mtu MTU value to set for the interface.
+ * @return An `RtNetlinkLinkMessage` instance representing the request to query the interface.
+ */
+ @Nullable
+ public static RtNetlinkLinkMessage createSetMtuMessage(@NonNull String interfaceName,
+ int sequenceNumber, int mtu) {
+ return createSetMtuMessage(
+ interfaceName, sequenceNumber, mtu, new OsAccess());
+ }
+
+ @VisibleForTesting
+ @Nullable
+ protected static RtNetlinkLinkMessage createSetMtuMessage(@NonNull String interfaceName,
+ int sequenceNumber, int mtu, @NonNull OsAccess osAccess) {
+ final int interfaceIndex = osAccess.if_nametoindex(interfaceName);
+ if (interfaceIndex == OsAccess.INVALID_INTERFACE_INDEX) {
+ return null;
+ }
+ return RtNetlinkLinkMessage.build(
+ new StructNlMsgHdr(/*payloadLen*/ 0, RTM_NEWLINK, NLM_F_REQUEST_ACK , sequenceNumber),
+ new StructIfinfoMsg((short) AF_UNSPEC, /*type*/ 0, interfaceIndex,
+ /*flags*/ 0, /*change*/ 0),
+ mtu, /*hardwareAddress*/ null, /*interfaceName*/ null);
+ }
+
@Override
public String toString() {
return "RtNetlinkLinkMessage{ "
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
index 4878334..8d7ae5c 100644
--- a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
+++ b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
@@ -23,6 +23,7 @@
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
+import java.util.List;
/**
* Network constants used by the network stack.
@@ -153,7 +154,8 @@
(Inet6Address) InetAddresses.parseNumericAddress("ff02::2");
public static final Inet6Address IPV6_ADDR_ALL_HOSTS_MULTICAST =
(Inet6Address) InetAddresses.parseNumericAddress("ff02::3");
-
+ public static final Inet6Address IPV6_ADDR_NODE_LOCAL_ALL_NODES_MULTICAST =
+ (Inet6Address) InetAddresses.parseNumericAddress("ff01::1");
public static final int IPPROTO_FRAGMENT = 44;
/**
@@ -340,6 +342,26 @@
*/
public static final String TEST_URL_EXPIRATION_TIME = "test_url_expiration_time";
+ /**
+ * List of IpPrefix that are local network prefixes.
+ */
+ public static final List<IpPrefix> IPV4_LOCAL_PREFIXES = List.of(
+ new IpPrefix("169.254.0.0/16"), // Link Local
+ new IpPrefix("100.64.0.0/10"), // CGNAT
+ new IpPrefix("10.0.0.0/8"), // RFC1918
+ new IpPrefix("172.16.0.0/12"), // RFC1918
+ new IpPrefix("192.168.0.0/16") // RFC1918
+ );
+
+ /**
+ * List of IpPrefix that are multicast and broadcast prefixes.
+ */
+ public static final List<IpPrefix> MULTICAST_AND_BROADCAST_PREFIXES = List.of(
+ new IpPrefix("224.0.0.0/4"), // Multicast
+ new IpPrefix("ff00::/8"), // Multicast
+ new IpPrefix("255.255.255.255/32") // Broadcast
+ );
+
// TODO: Move to Inet4AddressUtils
// See aosp/1455936: NetworkStackConstants can't depend on it as it causes jarjar-related issues
// for users of both the net-utils-device-common and net-utils-framework-common libraries.
diff --git a/staticlibs/native/tcutils/tcutils.cpp b/staticlibs/native/tcutils/tcutils.cpp
index 21e781c..5425d0e 100644
--- a/staticlibs/native/tcutils/tcutils.cpp
+++ b/staticlibs/native/tcutils/tcutils.cpp
@@ -361,7 +361,7 @@
const sockaddr_nl KERNEL_NLADDR = {AF_NETLINK, 0, 0, 0};
const uint16_t NETLINK_REQUEST_FLAGS = NLM_F_REQUEST | NLM_F_ACK;
-int sendAndProcessNetlinkResponse(const void *req, int len) {
+int sendAndProcessNetlinkResponse(const void *req, int len, bool enoent_ok) {
// TODO: use unique_fd instead of ScopeGuard
unique_fd fd(socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE));
if (!fd.ok()) {
@@ -445,7 +445,9 @@
return -ENOMSG;
}
- if (resp.e.error) {
+ if (resp.e.error == -ENOENT) {
+ if (!enoent_ok) ALOGE("NLMSG_ERROR message returned ENOENT");
+ } else if (resp.e.error) {
ALOGE("NLMSG_ERROR message return error: %d", resp.e.error);
}
return resp.e.error; // returns 0 on success
@@ -560,7 +562,8 @@
};
#undef CLSACT
- return sendAndProcessNetlinkResponse(&req, sizeof(req));
+ const bool enoent_ok = (nlMsgType == RTM_DELQDISC);
+ return sendAndProcessNetlinkResponse(&req, sizeof(req), enoent_ok);
}
// tc filter add dev .. in/egress prio 1 protocol ipv6/ip bpf object-pinned
@@ -666,7 +669,7 @@
snprintf(req.options.name.str, sizeof(req.options.name.str), "%s:[*fsobj]",
basename(bpfProgPath));
- int error = sendAndProcessNetlinkResponse(&req, sizeof(req));
+ int error = sendAndProcessNetlinkResponse(&req, sizeof(req), false);
return error;
}
@@ -698,7 +701,8 @@
return error;
}
return sendAndProcessNetlinkResponse(filter.getRequest(),
- filter.getRequestSize());
+ filter.getRequestSize(),
+ false);
}
// tc filter del dev .. in/egress prio .. protocol ..
@@ -726,7 +730,7 @@
},
};
- return sendAndProcessNetlinkResponse(&req, sizeof(req));
+ return sendAndProcessNetlinkResponse(&req, sizeof(req), true);
}
} // namespace android
diff --git a/staticlibs/netd/Android.bp b/staticlibs/netd/Android.bp
index 44abba2..03f5f06 100644
--- a/staticlibs/netd/Android.bp
+++ b/staticlibs/netd/Android.bp
@@ -22,7 +22,7 @@
sdk_version: "system_current",
min_sdk_version: "30",
static_libs: [
- "netd_aidl_interface-V15-java",
+ "netd_aidl_interface-V16-java",
],
apex_available: [
"//apex_available:platform", // used from services.net
@@ -45,7 +45,7 @@
cc_library_static {
name: "netd_aidl_interface-lateststable-ndk",
whole_static_libs: [
- "netd_aidl_interface-V15-ndk",
+ "netd_aidl_interface-V16-ndk",
],
apex_available: [
"com.android.resolv",
@@ -56,12 +56,12 @@
cc_defaults {
name: "netd_aidl_interface_lateststable_cpp_static",
- static_libs: ["netd_aidl_interface-V15-cpp"],
+ static_libs: ["netd_aidl_interface-V16-cpp"],
}
cc_defaults {
name: "netd_aidl_interface_lateststable_cpp_shared",
- shared_libs: ["netd_aidl_interface-V15-cpp"],
+ shared_libs: ["netd_aidl_interface-V16-cpp"],
}
aidl_interface {
@@ -171,6 +171,10 @@
version: "15",
imports: [],
},
+ {
+ version: "16",
+ imports: [],
+ },
],
frozen: true,
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/.hash b/staticlibs/netd/aidl_api/netd_aidl_interface/16/.hash
new file mode 100644
index 0000000..08cd338
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/.hash
@@ -0,0 +1 @@
+28e20632b92e146787d32437a53aaa5ad39125b7
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/INetd.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/INetd.aidl
new file mode 100644
index 0000000..8351b56
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/INetd.aidl
@@ -0,0 +1,272 @@
+/**
+ * Copyright (c) 2016, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetd {
+ boolean isAlive();
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ boolean firewallReplaceUidChain(in @utf8InCpp String chainName, boolean isAllowlist, in int[] uids);
+ boolean bandwidthEnableDataSaver(boolean enable);
+ /**
+ * @deprecated use networkCreate() instead.
+ */
+ void networkCreatePhysical(int netId, int permission);
+ /**
+ * @deprecated use networkCreate() instead.
+ */
+ void networkCreateVpn(int netId, boolean secure);
+ void networkDestroy(int netId);
+ void networkAddInterface(int netId, in @utf8InCpp String iface);
+ void networkRemoveInterface(int netId, in @utf8InCpp String iface);
+ void networkAddUidRanges(int netId, in android.net.UidRangeParcel[] uidRanges);
+ void networkRemoveUidRanges(int netId, in android.net.UidRangeParcel[] uidRanges);
+ void networkRejectNonSecureVpn(boolean add, in android.net.UidRangeParcel[] uidRanges);
+ void socketDestroy(in android.net.UidRangeParcel[] uidRanges, in int[] exemptUids);
+ boolean tetherApplyDnsInterfaces();
+ android.net.TetherStatsParcel[] tetherGetStats();
+ void interfaceAddAddress(in @utf8InCpp String ifName, in @utf8InCpp String addrString, int prefixLength);
+ void interfaceDelAddress(in @utf8InCpp String ifName, in @utf8InCpp String addrString, int prefixLength);
+ @utf8InCpp String getProcSysNet(int ipversion, int which, in @utf8InCpp String ifname, in @utf8InCpp String parameter);
+ void setProcSysNet(int ipversion, int which, in @utf8InCpp String ifname, in @utf8InCpp String parameter, in @utf8InCpp String value);
+ void ipSecSetEncapSocketOwner(in ParcelFileDescriptor socket, int newUid);
+ int ipSecAllocateSpi(int transformId, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int spi);
+ void ipSecAddSecurityAssociation(int transformId, int mode, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int underlyingNetId, int spi, int markValue, int markMask, in @utf8InCpp String authAlgo, in byte[] authKey, in int authTruncBits, in @utf8InCpp String cryptAlgo, in byte[] cryptKey, in int cryptTruncBits, in @utf8InCpp String aeadAlgo, in byte[] aeadKey, in int aeadIcvBits, int encapType, int encapLocalPort, int encapRemotePort, int interfaceId);
+ void ipSecDeleteSecurityAssociation(int transformId, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int spi, int markValue, int markMask, int interfaceId);
+ void ipSecApplyTransportModeTransform(in ParcelFileDescriptor socket, int transformId, int direction, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int spi);
+ void ipSecRemoveTransportModeTransform(in ParcelFileDescriptor socket);
+ void ipSecAddSecurityPolicy(int transformId, int selAddrFamily, int direction, in @utf8InCpp String tmplSrcAddress, in @utf8InCpp String tmplDstAddress, int spi, int markValue, int markMask, int interfaceId);
+ void ipSecUpdateSecurityPolicy(int transformId, int selAddrFamily, int direction, in @utf8InCpp String tmplSrcAddress, in @utf8InCpp String tmplDstAddress, int spi, int markValue, int markMask, int interfaceId);
+ void ipSecDeleteSecurityPolicy(int transformId, int selAddrFamily, int direction, int markValue, int markMask, int interfaceId);
+ void ipSecAddTunnelInterface(in @utf8InCpp String deviceName, in @utf8InCpp String localAddress, in @utf8InCpp String remoteAddress, int iKey, int oKey, int interfaceId);
+ void ipSecUpdateTunnelInterface(in @utf8InCpp String deviceName, in @utf8InCpp String localAddress, in @utf8InCpp String remoteAddress, int iKey, int oKey, int interfaceId);
+ void ipSecRemoveTunnelInterface(in @utf8InCpp String deviceName);
+ void wakeupAddInterface(in @utf8InCpp String ifName, in @utf8InCpp String prefix, int mark, int mask);
+ void wakeupDelInterface(in @utf8InCpp String ifName, in @utf8InCpp String prefix, int mark, int mask);
+ void setIPv6AddrGenMode(in @utf8InCpp String ifName, int mode);
+ void idletimerAddInterface(in @utf8InCpp String ifName, int timeout, in @utf8InCpp String classLabel);
+ void idletimerRemoveInterface(in @utf8InCpp String ifName, int timeout, in @utf8InCpp String classLabel);
+ void strictUidCleartextPenalty(int uid, int policyPenalty);
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The clatd control plane moved to the mainline module starting in T. See ClatCoordinator.
+ */
+ @utf8InCpp String clatdStart(in @utf8InCpp String ifName, in @utf8InCpp String nat64Prefix);
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The clatd control plane moved to the mainline module starting in T. See ClatCoordinator.
+ */
+ void clatdStop(in @utf8InCpp String ifName);
+ boolean ipfwdEnabled();
+ @utf8InCpp String[] ipfwdGetRequesterList();
+ void ipfwdEnableForwarding(in @utf8InCpp String requester);
+ void ipfwdDisableForwarding(in @utf8InCpp String requester);
+ void ipfwdAddInterfaceForward(in @utf8InCpp String fromIface, in @utf8InCpp String toIface);
+ void ipfwdRemoveInterfaceForward(in @utf8InCpp String fromIface, in @utf8InCpp String toIface);
+ void bandwidthSetInterfaceQuota(in @utf8InCpp String ifName, long bytes);
+ void bandwidthRemoveInterfaceQuota(in @utf8InCpp String ifName);
+ void bandwidthSetInterfaceAlert(in @utf8InCpp String ifName, long bytes);
+ void bandwidthRemoveInterfaceAlert(in @utf8InCpp String ifName);
+ void bandwidthSetGlobalAlert(long bytes);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void bandwidthAddNaughtyApp(int uid);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void bandwidthRemoveNaughtyApp(int uid);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void bandwidthAddNiceApp(int uid);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void bandwidthRemoveNiceApp(int uid);
+ void tetherStart(in @utf8InCpp String[] dhcpRanges);
+ void tetherStop();
+ boolean tetherIsEnabled();
+ void tetherInterfaceAdd(in @utf8InCpp String ifName);
+ void tetherInterfaceRemove(in @utf8InCpp String ifName);
+ @utf8InCpp String[] tetherInterfaceList();
+ void tetherDnsSet(int netId, in @utf8InCpp String[] dnsAddrs);
+ @utf8InCpp String[] tetherDnsList();
+ void networkAddRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop);
+ void networkRemoveRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop);
+ void networkAddLegacyRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop, int uid);
+ void networkRemoveLegacyRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop, int uid);
+ int networkGetDefault();
+ void networkSetDefault(int netId);
+ void networkClearDefault();
+ void networkSetPermissionForNetwork(int netId, int permission);
+ void networkSetPermissionForUser(int permission, in int[] uids);
+ void networkClearPermissionForUser(in int[] uids);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void trafficSetNetPermForUids(int permission, in int[] uids);
+ void networkSetProtectAllow(int uid);
+ void networkSetProtectDeny(int uid);
+ boolean networkCanProtect(int uid);
+ void firewallSetFirewallType(int firewalltype);
+ void firewallSetInterfaceRule(in @utf8InCpp String ifName, int firewallRule);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void firewallSetUidRule(int childChain, int uid, int firewallRule);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void firewallEnableChildChain(int childChain, boolean enable);
+ @utf8InCpp String[] interfaceGetList();
+ android.net.InterfaceConfigurationParcel interfaceGetCfg(in @utf8InCpp String ifName);
+ void interfaceSetCfg(in android.net.InterfaceConfigurationParcel cfg);
+ void interfaceSetIPv6PrivacyExtensions(in @utf8InCpp String ifName, boolean enable);
+ void interfaceClearAddrs(in @utf8InCpp String ifName);
+ void interfaceSetEnableIPv6(in @utf8InCpp String ifName, boolean enable);
+ void interfaceSetMtu(in @utf8InCpp String ifName, int mtu);
+ void tetherAddForward(in @utf8InCpp String intIface, in @utf8InCpp String extIface);
+ void tetherRemoveForward(in @utf8InCpp String intIface, in @utf8InCpp String extIface);
+ void setTcpRWmemorySize(in @utf8InCpp String rmemValues, in @utf8InCpp String wmemValues);
+ void registerUnsolicitedEventListener(android.net.INetdUnsolicitedEventListener listener);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void firewallAddUidInterfaceRules(in @utf8InCpp String ifName, in int[] uids);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void firewallRemoveUidInterfaceRules(in int[] uids);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void trafficSwapActiveStatsMap();
+ IBinder getOemNetd();
+ void tetherStartWithConfiguration(in android.net.TetherConfigParcel config);
+ android.net.MarkMaskParcel getFwmarkForNetwork(int netId);
+ void networkAddRouteParcel(int netId, in android.net.RouteInfoParcel routeInfo);
+ void networkUpdateRouteParcel(int netId, in android.net.RouteInfoParcel routeInfo);
+ void networkRemoveRouteParcel(int netId, in android.net.RouteInfoParcel routeInfo);
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+ */
+ void tetherOffloadRuleAdd(in android.net.TetherOffloadRuleParcel rule);
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+ */
+ void tetherOffloadRuleRemove(in android.net.TetherOffloadRuleParcel rule);
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+ */
+ android.net.TetherStatsParcel[] tetherOffloadGetStats();
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+ */
+ void tetherOffloadSetInterfaceQuota(int ifIndex, long quotaBytes);
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+ */
+ android.net.TetherStatsParcel tetherOffloadGetAndClearStats(int ifIndex);
+ void networkCreate(in android.net.NativeNetworkConfig config);
+ void networkAddUidRangesParcel(in android.net.netd.aidl.NativeUidRangeConfig uidRangesConfig);
+ void networkRemoveUidRangesParcel(in android.net.netd.aidl.NativeUidRangeConfig uidRangesConfig);
+ void ipSecMigrate(in android.net.IpSecMigrateInfoParcel migrateInfo);
+ void setNetworkAllowlist(in android.net.netd.aidl.NativeUidRangeConfig[] allowedNetworks);
+ void networkAllowBypassVpnOnNetwork(boolean allow, int uid, int netId);
+ const int IPV4 = 4;
+ const int IPV6 = 6;
+ const int CONF = 1;
+ const int NEIGH = 2;
+ const String IPSEC_INTERFACE_PREFIX = "ipsec";
+ const int IPV6_ADDR_GEN_MODE_EUI64 = 0;
+ const int IPV6_ADDR_GEN_MODE_NONE = 1;
+ const int IPV6_ADDR_GEN_MODE_STABLE_PRIVACY = 2;
+ const int IPV6_ADDR_GEN_MODE_RANDOM = 3;
+ const int IPV6_ADDR_GEN_MODE_DEFAULT = 0;
+ const int PENALTY_POLICY_ACCEPT = 1;
+ const int PENALTY_POLICY_LOG = 2;
+ const int PENALTY_POLICY_REJECT = 3;
+ const int CLAT_MARK = 0xdeadc1a7;
+ const int LOCAL_NET_ID = 99;
+ const int DUMMY_NET_ID = 51;
+ const int UNREACHABLE_NET_ID = 52;
+ const String NEXTHOP_NONE = "";
+ const String NEXTHOP_UNREACHABLE = "unreachable";
+ const String NEXTHOP_THROW = "throw";
+ const int PERMISSION_NONE = 0;
+ const int PERMISSION_NETWORK = 1;
+ const int PERMISSION_SYSTEM = 2;
+ /**
+ * @deprecated usage is internal to module.
+ */
+ const int NO_PERMISSIONS = 0;
+ /**
+ * @deprecated usage is internal to module.
+ */
+ const int PERMISSION_INTERNET = 4;
+ /**
+ * @deprecated usage is internal to module.
+ */
+ const int PERMISSION_UPDATE_DEVICE_STATS = 8;
+ /**
+ * @deprecated usage is internal to module.
+ */
+ const int PERMISSION_UNINSTALLED = (-1) /* -1 */;
+ /**
+ * @deprecated use FIREWALL_ALLOWLIST.
+ */
+ const int FIREWALL_WHITELIST = 0;
+ const int FIREWALL_ALLOWLIST = 0;
+ /**
+ * @deprecated use FIREWALL_DENYLIST.
+ */
+ const int FIREWALL_BLACKLIST = 1;
+ const int FIREWALL_DENYLIST = 1;
+ const int FIREWALL_RULE_ALLOW = 1;
+ const int FIREWALL_RULE_DENY = 2;
+ const int FIREWALL_CHAIN_NONE = 0;
+ const int FIREWALL_CHAIN_DOZABLE = 1;
+ const int FIREWALL_CHAIN_STANDBY = 2;
+ const int FIREWALL_CHAIN_POWERSAVE = 3;
+ const int FIREWALL_CHAIN_RESTRICTED = 4;
+ const String IF_STATE_UP = "up";
+ const String IF_STATE_DOWN = "down";
+ const String IF_FLAG_BROADCAST = "broadcast";
+ const String IF_FLAG_LOOPBACK = "loopback";
+ const String IF_FLAG_POINTOPOINT = "point-to-point";
+ const String IF_FLAG_RUNNING = "running";
+ const String IF_FLAG_MULTICAST = "multicast";
+ const int IPSEC_DIRECTION_IN = 0;
+ const int IPSEC_DIRECTION_OUT = 1;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/INetdUnsolicitedEventListener.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/INetdUnsolicitedEventListener.aidl
new file mode 100644
index 0000000..31775df
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/INetdUnsolicitedEventListener.aidl
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2018, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetdUnsolicitedEventListener {
+ oneway void onInterfaceClassActivityChanged(boolean isActive, int timerLabel, long timestampNs, int uid);
+ oneway void onQuotaLimitReached(@utf8InCpp String alertName, @utf8InCpp String ifName);
+ oneway void onInterfaceDnsServerInfo(@utf8InCpp String ifName, long lifetimeS, in @utf8InCpp String[] servers);
+ oneway void onInterfaceAddressUpdated(@utf8InCpp String addr, @utf8InCpp String ifName, int flags, int scope);
+ oneway void onInterfaceAddressRemoved(@utf8InCpp String addr, @utf8InCpp String ifName, int flags, int scope);
+ oneway void onInterfaceAdded(@utf8InCpp String ifName);
+ oneway void onInterfaceRemoved(@utf8InCpp String ifName);
+ oneway void onInterfaceChanged(@utf8InCpp String ifName, boolean up);
+ oneway void onInterfaceLinkStateChanged(@utf8InCpp String ifName, boolean up);
+ oneway void onRouteChanged(boolean updated, @utf8InCpp String route, @utf8InCpp String gateway, @utf8InCpp String ifName);
+ oneway void onStrictCleartextDetected(int uid, @utf8InCpp String hex);
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/InterfaceConfigurationParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/InterfaceConfigurationParcel.aidl
new file mode 100644
index 0000000..1869d8d
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/InterfaceConfigurationParcel.aidl
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable InterfaceConfigurationParcel {
+ @utf8InCpp String ifName;
+ @utf8InCpp String hwAddr;
+ @utf8InCpp String ipv4Addr;
+ int prefixLength;
+ @utf8InCpp String[] flags;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/IpSecMigrateInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/IpSecMigrateInfoParcel.aidl
new file mode 100644
index 0000000..975a261
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/IpSecMigrateInfoParcel.aidl
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2022, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaOnlyImmutable
+parcelable IpSecMigrateInfoParcel {
+ int requestId;
+ int selAddrFamily;
+ int direction;
+ @utf8InCpp String oldSourceAddress;
+ @utf8InCpp String oldDestinationAddress;
+ @utf8InCpp String newSourceAddress;
+ @utf8InCpp String newDestinationAddress;
+ int interfaceId;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/MarkMaskParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/MarkMaskParcel.aidl
new file mode 100644
index 0000000..8ea20d1
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/MarkMaskParcel.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable MarkMaskParcel {
+ int mark;
+ int mask;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeNetworkConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeNetworkConfig.aidl
new file mode 100644
index 0000000..77d814b
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeNetworkConfig.aidl
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable NativeNetworkConfig {
+ int netId;
+ android.net.NativeNetworkType networkType = android.net.NativeNetworkType.PHYSICAL;
+ int permission;
+ boolean secure;
+ android.net.NativeVpnType vpnType = android.net.NativeVpnType.PLATFORM;
+ boolean excludeLocalRoutes = false;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeNetworkType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeNetworkType.aidl
new file mode 100644
index 0000000..e77a143
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeNetworkType.aidl
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@Backing(type="int")
+enum NativeNetworkType {
+ PHYSICAL = 0,
+ VIRTUAL = 1,
+ PHYSICAL_LOCAL = 2,
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeVpnType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeVpnType.aidl
new file mode 100644
index 0000000..8a8be83
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeVpnType.aidl
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@Backing(type="int")
+enum NativeVpnType {
+ SERVICE = 1,
+ PLATFORM = 2,
+ LEGACY = 3,
+ OEM = 4,
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/RouteInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/RouteInfoParcel.aidl
new file mode 100644
index 0000000..5ef95e6
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/RouteInfoParcel.aidl
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+parcelable RouteInfoParcel {
+ @utf8InCpp String destination;
+ @utf8InCpp String ifName;
+ @utf8InCpp String nextHop;
+ int mtu;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherConfigParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherConfigParcel.aidl
new file mode 100644
index 0000000..7b39c22
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherConfigParcel.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable TetherConfigParcel {
+ boolean usingLegacyDnsProxy;
+ @utf8InCpp String[] dhcpRanges;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherOffloadRuleParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherOffloadRuleParcel.aidl
new file mode 100644
index 0000000..983e986
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherOffloadRuleParcel.aidl
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable TetherOffloadRuleParcel {
+ int inputInterfaceIndex;
+ int outputInterfaceIndex;
+ byte[] destination;
+ int prefixLength;
+ byte[] srcL2Address;
+ byte[] dstL2Address;
+ int pmtu = 1500;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherStatsParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherStatsParcel.aidl
new file mode 100644
index 0000000..5f1b722
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherStatsParcel.aidl
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable TetherStatsParcel {
+ @utf8InCpp String iface;
+ long rxBytes;
+ long rxPackets;
+ long txBytes;
+ long txPackets;
+ int ifIndex = 0;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/UidRangeParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/UidRangeParcel.aidl
new file mode 100644
index 0000000..72e987a
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/UidRangeParcel.aidl
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable UidRangeParcel {
+ int start;
+ int stop;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/netd/aidl/NativeUidRangeConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/netd/aidl/NativeUidRangeConfig.aidl
new file mode 100644
index 0000000..9bb679f
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/netd/aidl/NativeUidRangeConfig.aidl
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.netd.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable NativeUidRangeConfig {
+ int netId;
+ android.net.UidRangeParcel[] uidRanges;
+ int subPriority;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/INetd.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/INetd.aidl
index 80b3b62..8351b56 100644
--- a/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/INetd.aidl
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/INetd.aidl
@@ -227,9 +227,21 @@
const int PERMISSION_NONE = 0;
const int PERMISSION_NETWORK = 1;
const int PERMISSION_SYSTEM = 2;
+ /**
+ * @deprecated usage is internal to module.
+ */
const int NO_PERMISSIONS = 0;
+ /**
+ * @deprecated usage is internal to module.
+ */
const int PERMISSION_INTERNET = 4;
+ /**
+ * @deprecated usage is internal to module.
+ */
const int PERMISSION_UPDATE_DEVICE_STATS = 8;
+ /**
+ * @deprecated usage is internal to module.
+ */
const int PERMISSION_UNINSTALLED = (-1) /* -1 */;
/**
* @deprecated use FIREWALL_ALLOWLIST.
diff --git a/staticlibs/netd/binder/android/net/INetd.aidl b/staticlibs/netd/binder/android/net/INetd.aidl
index e4c63b9..be8f538 100644
--- a/staticlibs/netd/binder/android/net/INetd.aidl
+++ b/staticlibs/netd/binder/android/net/INetd.aidl
@@ -933,24 +933,27 @@
/**
* NO_PERMISSIONS indicates that this app is installed and doesn't have either
* PERMISSION_INTERNET or PERMISSION_UPDATE_DEVICE_STATS.
- * TODO: use PERMISSION_NONE to represent this case
+ * @deprecated usage is internal to module.
*/
const int NO_PERMISSIONS = 0;
/**
- * PERMISSION_INTERNET indicates that the app can create AF_INET and AF_INET6 sockets
+ * PERMISSION_INTERNET indicates that the app can create AF_INET and AF_INET6 sockets.
+ * @deprecated usage is internal to module.
*/
const int PERMISSION_INTERNET = 4;
/**
* PERMISSION_UPDATE_DEVICE_STATS is used for system UIDs and privileged apps
- * that have the UPDATE_DEVICE_STATS permission
+ * that have the UPDATE_DEVICE_STATS permission.
+ * @deprecated usage is internal to module.
*/
const int PERMISSION_UPDATE_DEVICE_STATS = 8;
/**
* PERMISSION_UNINSTALLED is used when an app is uninstalled from the device. All internet
- * related permissions need to be cleaned
+ * related permissions need to be cleaned.
+ * @deprecated usage is internal to module.
*/
const int PERMISSION_UNINSTALLED = -1;
diff --git a/staticlibs/tests/unit/host/python/apf_utils_test.py b/staticlibs/tests/unit/host/python/apf_utils_test.py
index 419b338..348df3b 100644
--- a/staticlibs/tests/unit/host/python/apf_utils_test.py
+++ b/staticlibs/tests/unit/host/python/apf_utils_test.py
@@ -103,8 +103,12 @@
self, mock_adb_shell: MagicMock
) -> None:
mock_adb_shell.return_value = """
+45: rmnet29: <POINTOPOINT,MULTICAST,NOARP> mtu 1500 qdisc ...
+ link/ether 73:01:23:45:df:e3 brd ff:ff:ff:ff:ff:ff
46: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq ...
link/ether 72:05:77:82:21:e0 brd ff:ff:ff:ff:ff:ff
+47: wlan1: <BROADCAST,MULTICAST> mtu 1500 qdisc ...
+ link/ether 6a:16:81:ff:82:9b brd ff:ff:ff:ff:ff:ff
"""
mac_address = get_hardware_address(self.mock_ad, "wlan0")
asserts.assert_equal(mac_address, "72:05:77:82:21:E0")
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/BpfDumpTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/BpfDumpTest.java
index a66dacd..673d9e6 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/BpfDumpTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/BpfDumpTest.java
@@ -17,18 +17,13 @@
package com.android.net.module.util;
import static android.system.OsConstants.EPERM;
-import static android.system.OsConstants.R_OK;
-
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.doThrow;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
+import android.annotation.Nullable;
import android.system.ErrnoException;
-import android.system.Os;
import android.util.Pair;
import androidx.test.filters.SmallTest;
@@ -38,7 +33,6 @@
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.MockitoSession;
import java.io.PrintWriter;
import java.io.StringWriter;
@@ -129,9 +123,14 @@
assertTrue(dump.contains("key=789, val=123"));
}
- private String getDumpMapStatus(final IBpfMap<Struct.S32, Struct.S32> map) {
+ private String getDumpMapStatus(final IBpfMap<Struct.S32, Struct.S32> map,
+ @Nullable final BpfDump.Dependencies deps) {
final StringWriter sw = new StringWriter();
- BpfDump.dumpMapStatus(map, new PrintWriter(sw), "mapName", "mapPath");
+ if (deps == null) {
+ BpfDump.dumpMapStatus(map, new PrintWriter(sw), "mapName", "mapPath");
+ } else {
+ BpfDump.dumpMapStatus(map, new PrintWriter(sw), "mapName", "mapPath", deps);
+ }
return sw.toString();
}
@@ -139,25 +138,34 @@
public void testGetMapStatus() {
final IBpfMap<Struct.S32, Struct.S32> map =
new TestBpfMap<>(Struct.S32.class, Struct.S32.class);
- assertEquals("mapName: OK\n", getDumpMapStatus(map));
+ assertEquals("mapName: OK\n", getDumpMapStatus(map, null /* deps */));
}
@Test
- public void testGetMapStatusNull() {
- final MockitoSession session = mockitoSession()
- .spyStatic(Os.class)
- .startMocking();
- try {
- // Os.access succeeds
- doReturn(true).when(() -> Os.access("mapPath", R_OK));
- assertEquals("mapName: NULL(map is pinned to mapPath)\n", getDumpMapStatus(null));
+ public void testGetMapStatusNull_accessSucceed() {
+ // Os.access succeeds
+ assertEquals("mapName: NULL(map is pinned to mapPath)\n",
+ getDumpMapStatus(null /* map */,
+ new BpfDump.Dependencies() {
+ @Override
+ public boolean access(String path, int mode) {
+ return true;
+ }
+ })
+ );
+ }
- // Os.access throws EPERM
- doThrow(new ErrnoException("", EPERM)).when(() -> Os.access("mapPath", R_OK));
- assertEquals("mapName: NULL(map is not pinned to mapPath: Operation not permitted)\n",
- getDumpMapStatus(null));
- } finally {
- session.finishMocking();
- }
+ @Test
+ public void testGetMapStatusNull_accessThrow() {
+ // Os.access throws EPERM
+ assertEquals("mapName: NULL(map is not pinned to mapPath: Operation not permitted)\n",
+ getDumpMapStatus(null /* map */,
+ new BpfDump.Dependencies(){
+ @Override
+ public boolean access(String path, int mode) throws ErrnoException {
+ throw new ErrnoException("", EPERM);
+ }
+ })
+ );
}
}
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/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
index 8104e3a..13710b1 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
@@ -308,7 +308,7 @@
@Test
public void testCreateSetInterfaceFlagsMessage() {
final String expectedHexBytes =
- "20000000100001006824000000000000" // struct nlmsghdr
+ "20000000100005006824000000000000" // struct nlmsghdr
+ "00000000080000000100000001000100"; // struct ifinfomsg
final String interfaceName = "wlan0";
final int interfaceIndex = 8;
@@ -328,6 +328,28 @@
}
@Test
+ public void testCreateSetInterfaceMtuMessage() {
+ final String expectedHexBytes =
+ "280000001000050068240000000000000000000008000000" // struct nlmsghdr
+ + "000000000000000008000400DC050000"; // struct ifinfomsg
+ final String interfaceName = "wlan0";
+ final int interfaceIndex = 8;
+ final int sequenceNumber = 0x2468;
+ final int mtu = 1500;
+
+ when(mOsAccess.if_nametoindex(interfaceName)).thenReturn(interfaceIndex);
+
+ final RtNetlinkLinkMessage msg = RtNetlinkLinkMessage.createSetMtuMessage(
+ interfaceName,
+ sequenceNumber,
+ mtu,
+ mOsAccess);
+ assertNotNull(msg);
+ final byte[] bytes = msg.pack(ByteOrder.LITTLE_ENDIAN); // For testing.
+ assertEquals(expectedHexBytes, HexDump.toHexString(bytes));
+ }
+
+ @Test
public void testToString() {
final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWLINK_HEX);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN); // For testing.
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/CarrierConfigRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/CarrierConfigRule.kt
index a93ae3e..ae0de79 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/CarrierConfigRule.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/CarrierConfigRule.kt
@@ -18,28 +18,36 @@
import android.Manifest.permission.MODIFY_PHONE_STATE
import android.Manifest.permission.READ_PHONE_STATE
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.ConditionVariable
import android.os.PersistableBundle
import android.telephony.CarrierConfigManager
+import android.telephony.CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED
+import android.telephony.SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX
import android.util.Log
import androidx.test.platform.app.InstrumentationRegistry
import com.android.modules.utils.build.SdkLevel.isAtLeastU
import com.android.testutils.runAsShell
import com.android.testutils.tryTest
import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
private val TAG = CarrierConfigRule::class.simpleName
+private const val CARRIER_CONFIG_CHANGE_TIMEOUT_MS = 10_000L
/**
* A [TestRule] that helps set [CarrierConfigManager] overrides for tests and clean up the test
* configuration automatically on teardown.
*/
class CarrierConfigRule : TestRule {
- private val ccm by lazy { InstrumentationRegistry.getInstrumentation().context.getSystemService(
- CarrierConfigManager::class.java
- ) }
+ private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
+ private val ccm by lazy { context.getSystemService(CarrierConfigManager::class.java) }
// Map of (subId) -> (original values of overridden settings)
private val originalConfigs = mutableMapOf<Int, PersistableBundle>()
@@ -61,6 +69,33 @@
}
}
+ private class ConfigChangeReceiver(private val subId: Int) : BroadcastReceiver() {
+ val cv = ConditionVariable()
+ override fun onReceive(context: Context, intent: Intent) {
+ if (intent.action != ACTION_CARRIER_CONFIG_CHANGED ||
+ intent.getIntExtra(EXTRA_SUBSCRIPTION_INDEX, -1) != subId) {
+ return
+ }
+ // This may race with other config changes for the same subId, but there is no way to
+ // know which update is being reported, and querying the override would return the
+ // latest values even before the config is applied. Config changes should be rare, so it
+ // is unlikely they would happen exactly after the override applied here and cause
+ // flakes.
+ cv.open()
+ }
+ }
+
+ private fun overrideConfigAndWait(subId: Int, config: PersistableBundle) {
+ val changeReceiver = ConfigChangeReceiver(subId)
+ context.registerReceiver(changeReceiver, IntentFilter(ACTION_CARRIER_CONFIG_CHANGED))
+ ccm.overrideConfig(subId, config)
+ assertTrue(
+ changeReceiver.cv.block(CARRIER_CONFIG_CHANGE_TIMEOUT_MS),
+ "Timed out waiting for config change for subId $subId"
+ )
+ context.unregisterReceiver(changeReceiver)
+ }
+
/**
* Add carrier config overrides with the specified configuration.
*
@@ -79,7 +114,7 @@
originalConfig.putAll(previousValues)
runAsShell(MODIFY_PHONE_STATE) {
- ccm.overrideConfig(subId, config)
+ overrideConfigAndWait(subId, config)
}
}
@@ -93,10 +128,10 @@
runAsShell(MODIFY_PHONE_STATE) {
originalConfigs.forEach { (subId, config) ->
try {
- // Do not use overrideConfig with null, as it would reset configs that may
+ // Do not use null as the config to reset, as it would reset configs that may
// have been set by target preparers such as
// ConnectivityTestTargetPreparer / CarrierConfigSetupTest.
- ccm.overrideConfig(subId, config)
+ overrideConfigAndWait(subId, config)
} catch (e: Throwable) {
Log.e(TAG, "Error resetting carrier config for subId $subId")
}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt b/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
index c7d6850..4b9429b 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
@@ -430,19 +430,32 @@
* @param dumpsysCmd The dumpsys command to run (for example "connectivity").
* @param exceptionContext An exception to write a stacktrace to the dump for context.
*/
- fun collectDumpsys(dumpsysCmd: String, exceptionContext: Throwable? = null) {
- Log.i(TAG, "Collecting dumpsys $dumpsysCmd for test artifacts")
+ fun collectDumpsys(dumpsysCmd: String, exceptionContext: Throwable? = null) =
+ collectCommandOutput("dumpsys $dumpsysCmd", exceptionContext = exceptionContext)
+
+ /**
+ * Add the output of a command to the test data dump.
+ *
+ * <p>The output will be collected immediately, and exported to a test artifact file when the
+ * test ends.
+ * @param cmd The command to run. Stdout of the command will be collected.
+ * @param shell The shell to run the command in.
+ * @param exceptionContext An exception to write a stacktrace to the dump for context.
+ */
+ fun collectCommandOutput(
+ cmd: String,
+ shell: String = "sh",
+ exceptionContext: Throwable? = null
+ ) {
+ Log.i(TAG, "Collecting '$cmd' for test artifacts")
PrintWriter(buffer).let {
- it.println("--- Dumpsys $dumpsysCmd at ${ZonedDateTime.now()} ---")
+ it.println("--- $cmd at ${ZonedDateTime.now()} ---")
maybeWriteExceptionContext(it, exceptionContext)
it.flush()
}
- ParcelFileDescriptor.AutoCloseInputStream(
- InstrumentationRegistry.getInstrumentation().uiAutomation.executeShellCommand(
- "dumpsys $dumpsysCmd"
- )
- ).use {
- it.copyTo(buffer)
+
+ runCommandInShell(cmd, shell) { stdout, _ ->
+ stdout.copyTo(buffer)
}
}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PollingUtils.kt b/staticlibs/testutils/devicetests/com/android/testutils/PollingUtils.kt
new file mode 100644
index 0000000..a6e7ead
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PollingUtils.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.testutils
+
+private const val POLLING_INTERVAL_MS: Int = 100
+
+/** Calls condition() until it returns true or timeout occurs. */
+fun pollingCheck(timeout_ms: Long, condition: () -> Boolean): Boolean {
+ var polling_time = 0
+ do {
+ Thread.sleep(POLLING_INTERVAL_MS.toLong())
+ polling_time += POLLING_INTERVAL_MS
+ if (condition()) return true
+ } while (polling_time < timeout_ms)
+ return false
+}
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/staticlibs/testutils/devicetests/com/android/testutils/ShellUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/ShellUtil.kt
new file mode 100644
index 0000000..fadc2ab
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ShellUtil.kt
@@ -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.
+ */
+
+@file:JvmName("ShellUtil")
+
+package com.android.testutils
+
+import android.app.UiAutomation
+import android.os.ParcelFileDescriptor.AutoCloseInputStream
+import android.os.ParcelFileDescriptor.AutoCloseOutputStream
+import androidx.test.platform.app.InstrumentationRegistry
+import java.io.InputStream
+
+/**
+ * Run a command in a shell.
+ *
+ * Compared to [UiAutomation.executeShellCommand], this allows running commands with pipes and
+ * redirections. [UiAutomation.executeShellCommand] splits the command on spaces regardless of
+ * quotes, so it is not able to run commands like `sh -c "echo 123 > some_file"`.
+ *
+ * @param cmd Shell command to run.
+ * @param shell Command used to run the shell.
+ * @param outputProcessor Function taking stdout, stderr as argument. The streams will be closed
+ * when this function returns.
+ * @return Result of [outputProcessor].
+ */
+fun <T> runCommandInShell(
+ cmd: String,
+ shell: String = "sh",
+ outputProcessor: (InputStream, InputStream) -> T,
+): T {
+ val (stdout, stdin, stderr) = InstrumentationRegistry.getInstrumentation().uiAutomation
+ .executeShellCommandRwe(shell)
+ AutoCloseOutputStream(stdin).bufferedWriter().use { it.write(cmd) }
+ AutoCloseInputStream(stdout).use { outStream ->
+ AutoCloseInputStream(stderr).use { errStream ->
+ return outputProcessor(outStream, errStream)
+ }
+ }
+}
+
+/**
+ * Run a command in a shell.
+ *
+ * Overload of [runCommandInShell] that reads and returns stdout as String.
+ */
+fun runCommandInShell(
+ cmd: String,
+ shell: String = "sh",
+) = runCommandInShell(cmd, shell) { stdout, _ ->
+ stdout.reader().use { it.readText() }
+}
+
+/**
+ * Run a command in a root shell.
+ *
+ * This is generally only usable on devices on which [DeviceInfoUtils.isDebuggable] is true.
+ * @see runCommandInShell
+ */
+fun runCommandInRootShell(
+ cmd: String
+) = runCommandInShell(cmd, shell = "su root sh")
diff --git a/staticlibs/testutils/host/python/apf_utils.py b/staticlibs/testutils/host/python/apf_utils.py
index 55ac860..49ffad6 100644
--- a/staticlibs/testutils/host/python/apf_utils.py
+++ b/staticlibs/testutils/host/python/apf_utils.py
@@ -162,14 +162,18 @@
"""
# Run the "ip link" command and get its output.
- ip_link_output = adb_utils.adb_shell(ad, f"ip link show {iface_name}")
+ ip_link_output = adb_utils.adb_shell(ad, f"ip link")
# Regular expression to extract the MAC address.
# Parse hardware address from ip link output like below:
+ # 45: rmnet29: <POINTOPOINT,MULTICAST,NOARP> mtu 1500 qdisc ...
+ # link/ether 73:01:23:45:df:e3 brd ff:ff:ff:ff:ff:ff
# 46: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq ...
# link/ether 72:05:77:82:21:e0 brd ff:ff:ff:ff:ff:ff
- pattern = r"link/ether (([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2})"
- match = re.search(pattern, ip_link_output)
+ # 47: wlan1: <BROADCAST,MULTICAST> mtu 1500 qdisc ...
+ # link/ether 6a:16:81:ff:82:9b brd ff:ff:ff:ff:ff:ff"
+ pattern = rf"{iface_name}:.*?link/ether (([0-9a-fA-F]{{2}}:){{5}}[0-9a-fA-F]{{2}})"
+ match = re.search(pattern, ip_link_output, re.DOTALL)
if match:
return match.group(1).upper() # Extract the MAC address string.
diff --git a/staticlibs/testutils/host/python/tether_utils.py b/staticlibs/testutils/host/python/tether_utils.py
index c63de1f..710f8a8 100644
--- a/staticlibs/testutils/host/python/tether_utils.py
+++ b/staticlibs/testutils/host/python/tether_utils.py
@@ -95,7 +95,9 @@
hotspot_interface = server.startHotspot(test_ssid, test_passphrase)
# Make the client connects to the hotspot.
- client_network = client.connectToWifi(test_ssid, test_passphrase)
+ client_network = client.connectToWifi(
+ test_ssid, test_passphrase, upstream_type != UpstreamType.NONE
+ )
return hotspot_interface, client_network
diff --git a/tests/common/java/android/net/NetworkAgentConfigTest.kt b/tests/common/java/android/net/NetworkAgentConfigTest.kt
index fe869f8..d640a73 100644
--- a/tests/common/java/android/net/NetworkAgentConfigTest.kt
+++ b/tests/common/java/android/net/NetworkAgentConfigTest.kt
@@ -20,7 +20,6 @@
import androidx.test.runner.AndroidJUnit4
import com.android.modules.utils.build.SdkLevel.isAtLeastS
import com.android.modules.utils.build.SdkLevel.isAtLeastT
-import com.android.modules.utils.build.SdkLevel.isAtLeastV
import com.android.testutils.ConnectivityModuleTest
import com.android.testutils.assertParcelingIsLossless
import org.junit.Assert.assertEquals
@@ -48,9 +47,6 @@
setLocalRoutesExcludedForVpn(true)
setVpnRequiresValidation(true)
}
- if (isAtLeastV()) {
- setSkipNativeNetworkCreation(true)
- }
}.build()
assertParcelingIsLossless(config)
}
@@ -75,9 +71,6 @@
setLocalRoutesExcludedForVpn(true)
setVpnRequiresValidation(true)
}
- if (isAtLeastV()) {
- setSkipNativeNetworkCreation(true)
- }
}.build()
assertTrue(config.isExplicitlySelected())
@@ -86,9 +79,6 @@
assertFalse(config.isPartialConnectivityAcceptable())
assertTrue(config.isUnvalidatedConnectivityAcceptable())
assertEquals("TEST_NETWORK", config.getLegacyTypeName())
- if (isAtLeastV()) {
- assertTrue(config.shouldSkipNativeNetworkCreation())
- }
if (isAtLeastT()) {
assertTrue(config.areLocalRoutesExcludedForVpn())
assertTrue(config.isVpnValidationRequired())
diff --git a/tests/cts/hostside/Android.bp b/tests/cts/hostside/Android.bp
index 0ac9ce1..0b4375a 100644
--- a/tests/cts/hostside/Android.bp
+++ b/tests/cts/hostside/Android.bp
@@ -27,7 +27,7 @@
// Note that some of the test helper apps (e.g., CtsHostsideNetworkCapTestsAppSdk33) override
// this with older SDK versions.
// Also note that unlike android_test targets, "current" does not work: the target SDK is set to
- // something like "VanillaIceCream" instead of 100000. This means that the tests will not run on
+ // something like "VanillaIceCream" instead of 10000. This means that the tests will not run on
// released devices with errors such as "Requires development platform VanillaIceCream but this
// is a release platform".
target_sdk_version: "10000",
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java
index ac60b0f..a1cf968 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java
@@ -16,6 +16,8 @@
package com.android.cts.net.hostside;
+import static android.net.TetheringManager.TETHERING_WIFI;
+
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static org.junit.Assert.assertEquals;
@@ -71,6 +73,9 @@
mCtsTetheringUtils.startWifiTethering(mTetheringEventCallback, softApConfig);
assertNotNull(tetheringInterface);
assertEquals(softApConfig, tetheringInterface.getSoftApConfiguration());
+ assertEquals(new TetheringInterface(
+ TETHERING_WIFI, tetheringInterface.getInterface(), softApConfig),
+ tetheringInterface);
TetheringInterface tetheringInterfaceForApp2 =
mTetheringHelperClient.getTetheredWifiInterface();
assertNotNull(tetheringInterfaceForApp2);
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
index 3430196..d55df6f 100755
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
@@ -1843,11 +1843,11 @@
final DetailedBlockedStatusCallback remoteUidCallback = new DetailedBlockedStatusCallback();
// Create a TUN interface
- final FileDescriptor tunFd = runWithShellPermissionIdentity(() -> {
+ final ParcelFileDescriptor tunFd = runWithShellPermissionIdentity(() -> {
final TestNetworkManager tnm = mTestContext.getSystemService(TestNetworkManager.class);
final TestNetworkInterface iface = tnm.createTunInterface(List.of(
TEST_IP4_DST_ADDR, TEST_IP6_DST_ADDR));
- return iface.getFileDescriptor().getFileDescriptor();
+ return iface.getFileDescriptor();
}, MANAGE_TEST_NETWORKS);
// Create a remote UDP socket
@@ -1861,7 +1861,7 @@
remoteUidCallback.expectAvailableCallbacksWithBlockedReasonNone(network);
// The remote UDP socket can receive packets coming from the TUN interface
- checkBlockIncomingPacket(tunFd, remoteUdpFd, EXPECT_PASS);
+ checkBlockIncomingPacket(tunFd.getFileDescriptor(), remoteUdpFd, EXPECT_PASS);
// Lockdown uid that has the remote UDP socket
runWithShellPermissionIdentity(() -> {
@@ -1877,7 +1877,7 @@
if (SdkLevel.isAtLeastT()) {
// On T and above, lockdown rule drop packets not coming from lo regardless of the
// VPN connectivity.
- checkBlockIncomingPacket(tunFd, remoteUdpFd, EXPECT_BLOCK);
+ checkBlockIncomingPacket(tunFd.getFileDescriptor(), remoteUdpFd, EXPECT_BLOCK);
}
// Start the VPN that has default routes. This VPN should have interface filtering rule
@@ -1889,9 +1889,9 @@
null /* proxyInfo */, null /* underlyingNetworks */,
false /* isAlwaysMetered */);
- checkBlockIncomingPacket(tunFd, remoteUdpFd, EXPECT_BLOCK);
+ checkBlockIncomingPacket(tunFd.getFileDescriptor(), remoteUdpFd, EXPECT_BLOCK);
}, /* cleanup */ () -> {
- Os.close(tunFd);
+ tunFd.close();
}, /* cleanup */ () -> {
Os.close(remoteUdpFd);
}, /* cleanup */ () -> {
diff --git a/tests/cts/multidevices/Android.bp b/tests/cts/multidevices/Android.bp
index a082a95..c730b86 100644
--- a/tests/cts/multidevices/Android.bp
+++ b/tests/cts/multidevices/Android.bp
@@ -42,9 +42,4 @@
// Package the snippet with the mobly test
":connectivity_multi_devices_snippet",
],
- version: {
- py3: {
- embedded_launcher: true,
- },
- },
}
diff --git a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
index 6da7e9a..252052e 100644
--- a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
+++ b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
@@ -27,13 +27,11 @@
import android.net.NetworkRequest
import android.net.cts.util.CtsNetUtils
import android.net.cts.util.CtsTetheringUtils
-import android.net.wifi.ScanResult
import android.net.wifi.SoftApConfiguration
import android.net.wifi.SoftApConfiguration.SECURITY_TYPE_WPA2_PSK
import android.net.wifi.WifiConfiguration
import android.net.wifi.WifiInfo
import android.net.wifi.WifiManager
-import android.net.wifi.WifiNetworkSpecifier
import android.net.wifi.WifiSsid
import androidx.test.platform.app.InstrumentationRegistry
import com.android.compatibility.common.util.PropertyUtil
@@ -109,10 +107,7 @@
// Suppress warning because WifiManager methods to connect to a config are
// documented not to be deprecated for privileged users.
@Suppress("DEPRECATION")
- fun connectToWifi(ssid: String, passphrase: String): Long {
- val specifier = WifiNetworkSpecifier.Builder()
- .setBand(ScanResult.WIFI_BAND_24_GHZ)
- .build()
+ fun connectToWifi(ssid: String, passphrase: String, requireValidation: Boolean): Long {
val wifiConfig = WifiConfiguration()
wifiConfig.SSID = "\"" + ssid + "\""
wifiConfig.preSharedKey = "\"" + passphrase + "\""
@@ -141,7 +136,8 @@
return@runAsShell networkCallback.eventuallyExpect<CapabilitiesChanged> {
// Remove double quotes.
val ssidFromCaps = (WifiInfo::sanitizeSsid)(it.caps.ssid)
- ssidFromCaps == ssid && it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
+ ssidFromCaps == ssid && (!requireValidation ||
+ it.caps.hasCapability(NET_CAPABILITY_VALIDATED))
}.network.networkHandle
}
}
diff --git a/tests/cts/multidevices/snippet/Wifip2pMultiDevicesSnippet.kt b/tests/cts/multidevices/snippet/Wifip2pMultiDevicesSnippet.kt
index f8c9351..3816537 100644
--- a/tests/cts/multidevices/snippet/Wifip2pMultiDevicesSnippet.kt
+++ b/tests/cts/multidevices/snippet/Wifip2pMultiDevicesSnippet.kt
@@ -21,8 +21,8 @@
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
+import android.content.pm.PackageManager.FEATURE_WIFI_DIRECT
import android.net.MacAddress
-import android.net.wifi.WifiManager
import android.net.wifi.p2p.WifiP2pConfig
import android.net.wifi.p2p.WifiP2pDevice
import android.net.wifi.p2p.WifiP2pDeviceList
@@ -44,10 +44,6 @@
class Wifip2pMultiDevicesSnippet : Snippet {
private val context by lazy { InstrumentationRegistry.getInstrumentation().getTargetContext() }
- private val wifiManager by lazy {
- context.getSystemService(WifiManager::class.java)
- ?: fail("Could not get WifiManager service")
- }
private val wifip2pManager by lazy {
context.getSystemService(WifiP2pManager::class.java)
?: fail("Could not get WifiP2pManager service")
@@ -84,7 +80,7 @@
}
@Rpc(description = "Check whether the device supports Wi-Fi P2P.")
- fun isP2pSupported() = wifiManager.isP2pSupported()
+ fun isP2pSupported() = context.packageManager.hasSystemFeature(FEATURE_WIFI_DIRECT)
@Rpc(description = "Start Wi-Fi P2P")
fun startWifiP2p() {
diff --git a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
index 3ab6c0d..2a372ce 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
@@ -52,10 +50,9 @@
import android.os.Handler
import android.os.HandlerThread
import android.os.PowerManager
+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
@@ -89,7 +86,7 @@
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.pollingCheck
import com.android.testutils.waitForIdle
import com.google.common.truth.Expect
import com.google.common.truth.Truth.assertThat
@@ -115,8 +112,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
@@ -134,16 +129,6 @@
private val powerManager = context.getSystemService(PowerManager::class.java)!!
private val wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG)
- fun pollingCheck(condition: () -> Boolean, timeout_ms: Int): Boolean {
- var polling_time = 0
- do {
- Thread.sleep(POLLING_INTERVAL_MS.toLong())
- polling_time += POLLING_INTERVAL_MS
- if (condition()) return true
- } while (polling_time < timeout_ms)
- return false
- }
-
fun turnScreenOff() {
if (!wakeLock.isHeld()) wakeLock.acquire()
runShellCommandOrThrow("input keyevent KEYCODE_SLEEP")
@@ -165,7 +150,9 @@
// This is a workaround for b/366037029.
Thread.sleep(2000L)
} else {
- val result = pollingCheck({ powerManager.isInteractive() }, timeout_ms = 2000)
+ val result = pollingCheck(timeout_ms = 2000) {
+ powerManager.isInteractive()
+ }
assertThat(result).isEqualTo(interactive)
}
}
@@ -191,16 +178,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
@@ -423,6 +400,10 @@
assume().that(caps.apfVersionSupported).isAtLeast(version)
}
+ fun assumeNotCuttlefish() {
+ assume().that(SystemProperties.get("ro.product.board", "")).isNotEqualTo("cutf")
+ }
+
fun installProgram(bytes: ByteArray) {
val prog = bytes.toHexString()
val result = runShellCommandOrThrow("cmd network_stack apf $ifname install $prog").trim()
@@ -484,15 +465,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)
}
@@ -506,6 +487,7 @@
// should be turned on.
assume().that(getVsrApiLevel()).isAtLeast(34)
assumeApfVersionSupportAtLeast(4)
+ assumeNotCuttlefish()
// clear any active APF filter
clearApfMemory()
@@ -558,6 +540,7 @@
assume().that(getVsrApiLevel()).isAtLeast(34)
// Test v4 memory slots on both v4 and v6 interpreters.
assumeApfVersionSupportAtLeast(4)
+ assumeNotCuttlefish()
clearApfMemory()
val gen = ApfV4Generator(
caps.apfVersionSupported,
@@ -584,6 +567,13 @@
val program = gen.generate()
assertThat(program.size).isLessThan(counterRegion)
+ val randomProgram = ByteArray(1) { 0 } +
+ ByteArray(counterRegion - 1).also { Random.nextBytes(it) }
+ // There are known firmware bugs where they calculate the number of non-zero bytes within
+ // the program to determine the program length. Modify the test to first install a longer
+ // program before installing a program that do the program length check. This should help us
+ // catch these types of firmware bugs in CTS. (b/395545572)
+ installAndVerifyProgram(randomProgram)
installAndVerifyProgram(program)
// Trigger the program by sending a ping and waiting on the reply.
@@ -616,6 +606,7 @@
// should be turned on.
assume().that(getVsrApiLevel()).isAtLeast(34)
assumeApfVersionSupportAtLeast(4)
+ assumeNotCuttlefish()
clearApfMemory()
val gen = ApfV4Generator(
caps.apfVersionSupported,
@@ -658,6 +649,7 @@
@Test
fun testFilterAge16384thsIncreasesBetweenPackets() {
assumeApfVersionSupportAtLeast(6000)
+ assumeNotCuttlefish()
clearApfMemory()
val gen = ApfV6Generator(
caps.apfVersionSupported,
@@ -707,6 +699,7 @@
@Test
fun testReplyPing() {
assumeApfVersionSupportAtLeast(6000)
+ assumeNotCuttlefish()
installProgram(ByteArray(caps.maximumApfProgramSize) { 0 }) // Clear previous program
readProgram() // Ensure installation is complete
@@ -734,11 +727,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(
@@ -799,8 +792,12 @@
Log.i(TAG, "counter map: ${apfCounterTracker.counters}")
assertThat(replyPayloads.size).isEqualTo(expectReplyPayloads.size)
- for (i in replyPayloads.indices) {
- assertThat(replyPayloads[i]).isEqualTo(expectReplyPayloads[i])
+
+ // Sort the payload list before comparison to ensure consistency.
+ val sortedReplyPayloads = replyPayloads.sortedBy { it[0] }
+ val sortedExpectReplyPayloads = expectReplyPayloads.sortedBy { it[0] }
+ for (i in sortedReplyPayloads.indices) {
+ assertThat(sortedReplyPayloads[i]).isEqualTo(sortedExpectReplyPayloads[i])
}
}
}
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 9457a42..87c2b9e 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -90,7 +90,6 @@
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
-import static android.net.cts.util.CtsNetUtils.ConnectivityActionReceiver;
import static android.net.cts.util.CtsNetUtils.HTTP_PORT;
import static android.net.cts.util.CtsNetUtils.NETWORK_CALLBACK_ACTION;
import static android.net.cts.util.CtsNetUtils.TEST_HOST;
@@ -111,6 +110,7 @@
import static com.android.networkstack.apishim.ConstantsShim.BLOCKED_REASON_LOCKDOWN_VPN;
import static com.android.networkstack.apishim.ConstantsShim.BLOCKED_REASON_NONE;
import static com.android.networkstack.apishim.ConstantsShim.RECEIVER_EXPORTED;
+import static com.android.networkstack.apishim.ConstantsShim.RECEIVER_NOT_EXPORTED;
import static com.android.testutils.Cleanup.testAndCleanup;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static com.android.testutils.MiscAsserts.assertEventuallyTrue;
@@ -127,6 +127,7 @@
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
import android.annotation.NonNull;
@@ -177,6 +178,7 @@
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
+import android.os.ConditionVariable;
import android.os.Handler;
import android.os.Looper;
import android.os.MessageQueue;
@@ -922,6 +924,7 @@
public void testOpenConnection() throws Exception {
assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
assumeTrue(mPackageManager.hasSystemFeature(FEATURE_TELEPHONY));
+ assumeFalse(Build.MODEL.contains("Cuttlefish"));
Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();
Network cellNetwork = networkCallbackRule.requestCell();
@@ -1227,42 +1230,43 @@
* {@link #testRegisterNetworkCallback} except that a {@code PendingIntent} is used instead
* of a {@code NetworkCallback}.
*/
- @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
@Test
+ // This test is flaky before aosp/3482151 which fixed the issue in the ConnectivityService
+ // code. Unfortunately this means T can't be fixed, so don't run this test with a module
+ // that hasn't been updated.
+ @ConnectivityModuleTest
public void testRegisterNetworkCallback_withPendingIntent() {
- assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
+ final ConditionVariable received = new ConditionVariable();
- // Create a ConnectivityActionReceiver that has an IntentFilter for our locally defined
- // action, NETWORK_CALLBACK_ACTION.
- final IntentFilter filter = new IntentFilter();
- filter.addAction(NETWORK_CALLBACK_ACTION);
+ // Register a callback with intent and a request for any Internet-providing network,
+ // which should match the currently connected network.
+ final BroadcastReceiver receiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+ received.open();
+ }
+ };
- final ConnectivityActionReceiver receiver = new ConnectivityActionReceiver(
- mCm, ConnectivityManager.TYPE_WIFI, NetworkInfo.State.CONNECTED);
- final int flags = SdkLevel.isAtLeastT() ? RECEIVER_EXPORTED : 0;
- mContext.registerReceiver(receiver, filter, flags);
+ final int flags = SdkLevel.isAtLeastT() ? RECEIVER_NOT_EXPORTED : 0;
+ mContext.registerReceiver(receiver, new IntentFilter(NETWORK_CALLBACK_ACTION), flags);
// Create a broadcast PendingIntent for NETWORK_CALLBACK_ACTION.
final Intent intent = new Intent(NETWORK_CALLBACK_ACTION)
.setPackage(mContext.getPackageName());
- // While ConnectivityService would put extra info such as network or request id before
- // broadcasting the inner intent. The MUTABLE flag needs to be added accordingly.
final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0 /*requestCode*/,
intent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE);
- // We will register for a WIFI network being available or lost.
- mCm.registerNetworkCallback(makeWifiNetworkRequest(), pendingIntent);
+ // Register for a network providing Internet being available or lost.
+ final NetworkRequest nr = new NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .build();
+ mCm.registerNetworkCallback(nr, pendingIntent);
try {
- mCtsNetUtils.ensureWifiConnected();
-
- // Now we expect to get the Intent delivered notifying of the availability of the wifi
- // network even if it was already connected as a state-based action when the callback
- // is registered.
- assertTrue("Did not receive expected Intent " + intent + " for TRANSPORT_WIFI",
- receiver.waitForState());
- } catch (InterruptedException e) {
- fail("Broadcast receiver or NetworkCallback wait was interrupted.");
+ // Wait for delivery of the Intent notifying of the availability of the
+ // INTERNET-providing network. Test setup makes sure it's already connected.
+ assertTrue("Did not receive expected Intent " + intent + " for INTERNET",
+ received.block(NETWORK_CALLBACK_TIMEOUT_MS));
} finally {
mCm.unregisterNetworkCallback(pendingIntent);
pendingIntent.cancel();
@@ -1270,6 +1274,33 @@
}
}
+ // Up to R ConnectivityService can't be updated through mainline, and there was a bug
+ // where registering a callback with a canceled pending intent would crash the system.
+ @Test
+ // Running this test without aosp/3482151 will likely crash the device.
+ @ConnectivityModuleTest
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testRegisterNetworkCallback_pendingIntent_classNotFound() {
+ final Intent intent = new Intent()
+ .setClassName(mContext.getPackageName(), "NonExistent");
+ final PendingIntent pi = PendingIntent.getActivity(mContext, /* requestCode */ 1,
+ intent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE);
+
+ final NetworkRequest nr = new NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .build();
+ try {
+ // Before the fix delivered through Mainline, this used to crash the system, because
+ // trying to send the pending intent would throw which would prompt ConnectivityService
+ // to release the wake lock, but it would still send a finished notification at which
+ // point CS would try to release the wake lock again and crash.
+ mCm.registerNetworkCallback(nr, pi);
+ } finally {
+ mCm.unregisterNetworkCallback(pi);
+ pi.cancel();
+ }
+ }
+
private void runIdenticalPendingIntentsRequestTest(boolean useListen) throws Exception {
assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
@@ -1375,12 +1406,20 @@
}
@AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+ // This test is flaky before aosp/3482151 which fixed the issue in the ConnectivityService
+ // code. Unfortunately this means T can't be fixed, so don't run this test with a module
+ // that hasn't been updated.
+ @ConnectivityModuleTest
@Test
public void testRegisterNetworkRequest_identicalPendingIntents() throws Exception {
runIdenticalPendingIntentsRequestTest(false /* useListen */);
}
@AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+ // This test is flaky before aosp/3482151 which fixed the issue in the ConnectivityService
+ // code. Unfortunately this means T can't be fixed, so don't run this test with a module
+ // that hasn't been updated.
+ @ConnectivityModuleTest
@Test
public void testRegisterNetworkCallback_identicalPendingIntents() throws Exception {
runIdenticalPendingIntentsRequestTest(true /* useListen */);
@@ -2329,8 +2368,10 @@
// Verify that turning airplane mode off takes effect as expected.
// connectToCell only registers a request, it cannot / does not need to be called twice
- mCtsNetUtils.ensureWifiConnected();
- if (verifyWifi) waitForAvailable(wifiCb);
+ if (verifyWifi) {
+ mCtsNetUtils.ensureWifiConnected();
+ waitForAvailable(wifiCb);
+ }
if (supportTelephony) {
telephonyCb.eventuallyExpect(
CallbackEntry.AVAILABLE, CELL_DATA_AVAILABLE_TIMEOUT_MS);
@@ -4083,4 +4124,11 @@
// shims, and @IgnoreUpTo does not check that.
assumeTrue(TestUtils.shouldTestSApis());
}
+
+ @Test
+ public void testLegacyTetherApisThrowUnsupportedOperationExceptionAfterV() {
+ assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM);
+ assertThrows(UnsupportedOperationException.class, () -> mCm.tether("iface"));
+ assertThrows(UnsupportedOperationException.class, () -> mCm.untether("iface"));
+ }
}
diff --git a/tests/cts/net/src/android/net/cts/DnsTest.java b/tests/cts/net/src/android/net/cts/DnsTest.java
index b1e5680..e367b7d 100644
--- a/tests/cts/net/src/android/net/cts/DnsTest.java
+++ b/tests/cts/net/src/android/net/cts/DnsTest.java
@@ -18,6 +18,7 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeFalse;
import android.content.Context;
import android.content.pm.PackageManager;
@@ -26,6 +27,7 @@
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkInfo;
+import android.os.Build;
import android.os.SystemClock;
import android.util.Log;
@@ -299,6 +301,8 @@
}
private void ensureIpv6Connectivity() throws InterruptedException {
+ assumeFalse(Build.MODEL.contains("Cuttlefish"));
+
CountDownLatch latch = new CountDownLatch(1);
final int TIMEOUT_MS = 5_000;
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 5b2c9f7..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(
@@ -605,6 +611,9 @@
}
private fun assumeNoInterfaceForTetheringAvailable() {
+ // Requesting a tethered interface will stop IpClient. Prevent it from doing so
+ // if adb is connected over ethernet.
+ assumeFalse(isAdbOverEthernet())
// Interfaces that have configured NetworkCapabilities will never be used for tethering,
// see aosp/2123900.
try {
@@ -1069,6 +1078,9 @@
@Test
fun testSetTetheringInterfaceMode_disableEnableEthernet() {
+ // do not run this test if an interface that can be used for tethering already exists.
+ assumeNoInterfaceForTetheringAvailable()
+
val listener = EthernetStateListener()
addInterfaceStateListener(listener)
diff --git a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
index e94d94f..4a21f09 100644
--- a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
@@ -375,6 +375,7 @@
}
private static boolean isIpv6UdpEncapSupportedByKernel() {
+ if (SdkLevel.isAtLeastB() && isKernelVersionAtLeast("5.10.0")) return true;
return isKernelVersionAtLeast("5.15.31")
|| (isKernelVersionAtLeast("5.10.108") && !isKernelVersionAtLeast("5.15.0"));
}
@@ -390,8 +391,8 @@
assumeTrue("Not supported by kernel", isIpv6UdpEncapSupportedByKernel());
}
- // TODO: b/319532485 Figure out whether to support x86_32
private static boolean isRequestTransformStateSupportedByKernel() {
+ if (SdkLevel.isAtLeastB()) return true;
return NetworkUtils.isKernel64Bit() || !NetworkUtils.isKernelX86();
}
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index 286e08c..8fcc703 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -722,9 +722,15 @@
val cv = ConditionVariable()
val cpb = PrivilegeWaiterCallback(cv)
- tryTest {
+ // The lambda below is capturing |cpb|, whose type inherits from a class that appeared in
+ // T. This means the lambda will compile as a private method of this class taking a
+ // PrivilegeWaiterCallback argument. As JUnit uses reflection to enumerate all methods
+ // including private methods, this would fail with a link error when running on S-.
+ // To solve this, make the lambda serializable, which causes the compiler to emit a
+ // synthetic class instead of a synthetic method.
+ tryTest @JvmSerializableLambda {
val slotIndex = SubscriptionManager.getSlotIndex(subId)!!
- runAsShell(READ_PRIVILEGED_PHONE_STATE) {
+ runAsShell(READ_PRIVILEGED_PHONE_STATE) @JvmSerializableLambda {
tm.registerCarrierPrivilegesCallback(slotIndex, { it.run() }, cpb)
}
// Wait for the callback to be registered
@@ -737,7 +743,6 @@
}
return@tryTest
}
- cv.close()
if (hold) {
carrierConfigRule.addConfigOverrides(subId, PersistableBundle().also {
it.putStringArray(CarrierConfigManager.KEY_CARRIER_CERTIFICATE_STRING_ARRAY,
@@ -746,9 +751,8 @@
} else {
carrierConfigRule.cleanUpNow()
}
- assertTrue(cv.block(DEFAULT_TIMEOUT_MS), "Can't change carrier privilege")
- } cleanup {
- runAsShell(READ_PRIVILEGED_PHONE_STATE) {
+ } cleanup @JvmSerializableLambda {
+ runAsShell(READ_PRIVILEGED_PHONE_STATE) @JvmSerializableLambda {
tm.unregisterCarrierPrivilegesCallback(cpb)
}
}
@@ -762,9 +766,15 @@
val cv = ConditionVariable()
val cpb = CarrierServiceChangedWaiterCallback(cv)
- tryTest {
+ // The lambda below is capturing |cpb|, whose type inherits from a class that appeared in
+ // T. This means the lambda will compile as a private method of this class taking a
+ // PrivilegeWaiterCallback argument. As JUnit uses reflection to enumerate all methods
+ // including private methods, this would fail with a link error when running on S-.
+ // To solve this, make the lambda serializable, which causes the compiler to emit a
+ // synthetic class instead of a synthetic method.
+ tryTest @JvmSerializableLambda {
val slotIndex = SubscriptionManager.getSlotIndex(subId)!!
- runAsShell(READ_PRIVILEGED_PHONE_STATE) {
+ runAsShell(READ_PRIVILEGED_PHONE_STATE) @JvmSerializableLambda {
tm.registerCarrierPrivilegesCallback(slotIndex, { it.run() }, cpb)
}
// Wait for the callback to be registered
@@ -786,8 +796,8 @@
}
}
assertTrue(cv.block(DEFAULT_TIMEOUT_MS), "Can't modify carrier service package")
- } cleanup {
- runAsShell(READ_PRIVILEGED_PHONE_STATE) {
+ } cleanup @JvmSerializableLambda {
+ runAsShell(READ_PRIVILEGED_PHONE_STATE) @JvmSerializableLambda {
tm.unregisterCarrierPrivilegesCallback(cpb)
}
}
diff --git a/tests/cts/net/src/android/net/cts/NetworkReservationTest.kt b/tests/cts/net/src/android/net/cts/NetworkReservationTest.kt
index f05bf15..f43b927 100644
--- a/tests/cts/net/src/android/net/cts/NetworkReservationTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkReservationTest.kt
@@ -16,15 +16,23 @@
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.bluetooth.BluetoothManager
+import android.content.pm.PackageManager.FEATURE_BLUETOOTH_LE
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
@@ -35,6 +43,7 @@
import android.os.HandlerThread
import android.platform.test.annotations.AppModeFull
import androidx.test.platform.app.InstrumentationRegistry
+import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
import com.android.testutils.ConnectivityModuleTest
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
@@ -42,9 +51,15 @@
import com.android.testutils.RecorderCallback.CallbackEntry.Unavailable
import com.android.testutils.TestableNetworkCallback
import com.android.testutils.TestableNetworkOfferCallback
+import com.android.testutils.pollingCheck
import com.android.testutils.runAsShell
+import kotlin.test.assertContains
import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
import org.junit.After
+import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -83,6 +98,10 @@
private val handler = Handler(handlerThread.looper)
private val provider = NetworkProvider(context, handlerThread.looper, TAG)
+ private val registeredCallbacks = ArrayList<TestableNetworkCallback>()
+ private val bm = context.getSystemService(BluetoothManager::class.java)!!
+ private var disableBluetoothInTearDown = false
+
@Before
fun setUp() {
runAsShell(NETWORK_SETTINGS) {
@@ -90,20 +109,60 @@
}
}
+ private fun enableBluetooth() {
+ val adapter = bm.adapter
+ assertNotNull(adapter)
+ if (adapter.isEnabled()) return
+
+ runShellCommandOrThrow("svc bluetooth enable")
+ val bluetoothEnabled = pollingCheck(TIMEOUT_MS) {
+ adapter.isEnabled()
+ }
+ assertTrue(bluetoothEnabled)
+ // Only disable Bluetooth in tear down when it hasn't already been enabled.
+ disableBluetoothInTearDown = true
+ }
+
+ private fun disableBluetooth() {
+ // adapter can't actually be null here, because this function does not run unless
+ // disableBluetoothInTearDown is true. Just in case, refrain from throwing an exception in
+ // tearDown.
+ val adapter = bm.adapter
+ if (adapter == null) return
+
+ runShellCommandOrThrow("svc bluetooth disable")
+ // Wait for #isEnabled() to return false; ignore failures.
+ pollingCheck(TIMEOUT_MS) {
+ !adapter.isEnabled()
+ }
+ }
+
@After
fun tearDown() {
+ registeredCallbacks.forEach { cm.unregisterNetworkCallback(it) }
runAsShell(NETWORK_SETTINGS) {
// unregisterNetworkProvider unregisters all associated NetworkOffers.
cm.unregisterNetworkProvider(provider)
}
handlerThread.quitSafely()
handlerThread.join()
+
+ if (disableBluetoothInTearDown) {
+ disableBluetooth()
+ }
}
fun NetworkCapabilities.copyWithReservationId(resId: Int) = NetworkCapabilities(this).also {
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 +171,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 +195,31 @@
provider.unregisterNetworkOffer(reservedOffer)
cb.expect<Unavailable>()
}
+
+ @Test
+ fun testReserveL2capNetwork() {
+ assumeTrue(context.packageManager.hasSystemFeature(FEATURE_BLUETOOTH_LE))
+ enableBluetooth()
+
+ 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/cts/net/src/android/net/cts/TrafficStatsTest.java b/tests/cts/net/src/android/net/cts/TrafficStatsTest.java
index bd9e03c..f5198e3 100755
--- a/tests/cts/net/src/android/net/cts/TrafficStatsTest.java
+++ b/tests/cts/net/src/android/net/cts/TrafficStatsTest.java
@@ -24,6 +24,8 @@
import android.util.Log;
import android.util.Range;
+import com.android.testutils.ConnectivityDiagnosticsCollector;
+
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -94,11 +96,12 @@
long tcpPacketToIpBytes(long packetCount, long bytes) {
// ip header + tcp header + data.
- // Tcp header is mostly 32. Syn has different tcp options -> 40. Don't care.
+ // Tcp header is mostly 32. Syn has different tcp options -> 40.
return packetCount * (20 + 32 + bytes);
}
@AppModeFull(reason = "Socket cannot bind in instant app mode")
+ @ConnectivityDiagnosticsCollector.CollectTcpdumpOnFailure
public void testTrafficStatsForLocalhost() throws IOException {
final long mobileTxPacketsBefore = TrafficStats.getMobileTxPackets();
final long mobileRxPacketsBefore = TrafficStats.getMobileRxPackets();
@@ -224,9 +227,15 @@
- uidTxDeltaPackets;
final long deltaRxOtherPackets = (totalRxPacketsAfter - totalRxPacketsBefore)
- uidRxDeltaPackets;
- if (deltaTxOtherPackets > 0 || deltaRxOtherPackets > 0) {
+ final long deltaTxOtherPktBytes = (totalTxBytesAfter - totalTxBytesBefore)
+ - uidTxDeltaBytes;
+ final long deltaRxOtherPktBytes = (totalRxBytesAfter - totalRxBytesBefore)
+ - uidRxDeltaBytes;
+ if (deltaTxOtherPackets != 0 || deltaRxOtherPackets != 0
+ || deltaTxOtherPktBytes != 0 || deltaRxOtherPktBytes != 0) {
Log.i(LOG_TAG, "lingering traffic data: " + deltaTxOtherPackets + "/"
- + deltaRxOtherPackets);
+ + deltaRxOtherPackets + "/" + deltaTxOtherPktBytes
+ + "/" + deltaRxOtherPktBytes);
}
// Check that the per-uid stats obtained from data profiling contain the expected values.
@@ -237,9 +246,9 @@
final long pktBytes = tcpPacketToIpBytes(packetCount, byteCount);
final long pktWithNoDataBytes = tcpPacketToIpBytes(packetCount, 0);
final long minExpExtraPktBytes = tcpPacketToIpBytes(minExpectedExtraPackets, 0);
- final long maxExpExtraPktBytes = tcpPacketToIpBytes(maxExpectedExtraPackets, 0);
- final long deltaTxOtherPktBytes = tcpPacketToIpBytes(deltaTxOtherPackets, 0);
- final long deltaRxOtherPktBytes = tcpPacketToIpBytes(deltaRxOtherPackets, 0);
+ // Syn/syn-ack has different tcp options, make tcp header 40 for upper bound estimation.
+ final long maxExpExtraPktBytes = tcpPacketToIpBytes(maxExpectedExtraPackets, 8);
+
assertInRange("txPackets detail", entry.txPackets, packetCount + minExpectedExtraPackets,
uidTxDeltaPackets);
assertInRange("rxPackets detail", entry.rxPackets, packetCount + minExpectedExtraPackets,
@@ -257,32 +266,24 @@
assertInRange("uidrxb", uidRxDeltaBytes, pktBytes + minExpExtraPktBytes,
pktBytes + pktWithNoDataBytes + maxExpExtraPktBytes + deltaRxOtherPktBytes);
assertInRange("iftxp", ifaceTxDeltaPackets, packetCount + minExpectedExtraPackets,
- packetCount + packetCount + maxExpectedExtraPackets);
+ packetCount + packetCount + maxExpectedExtraPackets + deltaTxOtherPackets);
assertInRange("ifrxp", ifaceRxDeltaPackets, packetCount + minExpectedExtraPackets,
- packetCount + packetCount + maxExpectedExtraPackets);
+ packetCount + packetCount + maxExpectedExtraPackets + deltaRxOtherPackets);
assertInRange("iftxb", ifaceTxDeltaBytes, pktBytes + minExpExtraPktBytes,
- pktBytes + pktWithNoDataBytes + maxExpExtraPktBytes);
+ pktBytes + pktWithNoDataBytes + maxExpExtraPktBytes + deltaTxOtherPktBytes);
assertInRange("ifrxb", ifaceRxDeltaBytes, pktBytes + minExpExtraPktBytes,
- pktBytes + pktWithNoDataBytes + maxExpExtraPktBytes);
+ pktBytes + pktWithNoDataBytes + maxExpExtraPktBytes + deltaRxOtherPktBytes);
// Localhost traffic *does* count against total stats.
// Check the total stats increased after test data transfer over localhost has been made.
- assertTrue("ttxp: " + totalTxPacketsBefore + " -> " + totalTxPacketsAfter,
- totalTxPacketsAfter >= totalTxPacketsBefore + uidTxDeltaPackets);
- assertTrue("trxp: " + totalRxPacketsBefore + " -> " + totalRxPacketsAfter,
- totalRxPacketsAfter >= totalRxPacketsBefore + uidRxDeltaPackets);
- assertTrue("ttxb: " + totalTxBytesBefore + " -> " + totalTxBytesAfter,
- totalTxBytesAfter >= totalTxBytesBefore + uidTxDeltaBytes);
- assertTrue("trxb: " + totalRxBytesBefore + " -> " + totalRxBytesAfter,
- totalRxBytesAfter >= totalRxBytesBefore + uidRxDeltaBytes);
- assertTrue("iftxp: " + ifaceTxPacketsBefore + " -> " + ifaceTxPacketsAfter,
- totalTxPacketsAfter >= totalTxPacketsBefore + ifaceTxDeltaPackets);
- assertTrue("ifrxp: " + ifaceRxPacketsBefore + " -> " + ifaceRxPacketsAfter,
- totalRxPacketsAfter >= totalRxPacketsBefore + ifaceRxDeltaPackets);
- assertTrue("iftxb: " + ifaceTxBytesBefore + " -> " + ifaceTxBytesAfter,
- totalTxBytesAfter >= totalTxBytesBefore + ifaceTxDeltaBytes);
- assertTrue("ifrxb: " + ifaceRxBytesBefore + " -> " + ifaceRxBytesAfter,
- totalRxBytesAfter >= totalRxBytesBefore + ifaceRxDeltaBytes);
+ assertInRange("ttxp", totalTxPacketsAfter,
+ totalTxPacketsBefore + packetCount + minExpectedExtraPackets, Long.MAX_VALUE);
+ assertInRange("trxp", totalRxPacketsAfter,
+ totalRxPacketsBefore + packetCount + minExpectedExtraPackets, Long.MAX_VALUE);
+ assertInRange("ttxb", totalTxBytesAfter,
+ totalTxBytesBefore + pktBytes + minExpExtraPktBytes, Long.MAX_VALUE);
+ assertInRange("trxb", totalRxBytesAfter,
+ totalRxBytesBefore + pktBytes + minExpExtraPktBytes, Long.MAX_VALUE);
// Localhost traffic should *not* count against mobile stats,
// There might be some other traffic, but nowhere near 1MB.
diff --git a/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java b/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java
index 243cd27..27cba3a 100644
--- a/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java
+++ b/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java
@@ -22,6 +22,7 @@
import static android.Manifest.permission.TETHER_PRIVILEGED;
import static android.net.TetheringManager.TETHERING_WIFI;
import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_REQUEST;
import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_FAILED;
import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STARTED;
import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STOPPED;
@@ -126,6 +127,71 @@
}
}
+ public static class StopTetheringCallback implements TetheringManager.StopTetheringCallback {
+ private static final int TIMEOUT_MS = 30_000;
+ public static class CallbackValue {
+ public final int error;
+
+ private CallbackValue(final int e) {
+ error = e;
+ }
+
+ public static class OnStopTetheringSucceeded extends CallbackValue {
+ OnStopTetheringSucceeded() {
+ super(TETHER_ERROR_NO_ERROR);
+ }
+ }
+
+ public static class OnStopTetheringFailed extends CallbackValue {
+ OnStopTetheringFailed(final int error) {
+ super(error);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s(%d)", getClass().getSimpleName(), error);
+ }
+ }
+
+ private final ArrayTrackRecord<CallbackValue>.ReadHead mHistory =
+ new ArrayTrackRecord<CallbackValue>().newReadHead();
+
+ @Override
+ public void onStopTetheringSucceeded() {
+ mHistory.add(new CallbackValue.OnStopTetheringSucceeded());
+ // Call the parent method so that the coverage linter sees it: http://b/385014495
+ TetheringManager.StopTetheringCallback.super.onStopTetheringSucceeded();
+ }
+
+ @Override
+ public void onStopTetheringFailed(final int error) {
+ mHistory.add(new CallbackValue.OnStopTetheringFailed(error));
+ // Call the parent method so that the coverage linter sees it: http://b/385014495
+ TetheringManager.StopTetheringCallback.super.onStopTetheringFailed(error);
+ }
+
+ /**
+ * Verifies that {@link #onStopTetheringSucceeded()} was called
+ */
+ public void verifyStopTetheringSucceeded() {
+ final CallbackValue cv = mHistory.poll(TIMEOUT_MS, c -> true);
+ assertNotNull("No onStopTetheringSucceeded after " + TIMEOUT_MS + " ms", cv);
+ assertTrue("Fail stop tethering:" + cv,
+ cv instanceof CallbackValue.OnStopTetheringSucceeded);
+ }
+
+ /**
+ * Verifies that {@link #onStopTetheringFailed(int)} was called
+ */
+ public void expectStopTetheringFailed(final int expected) {
+ final CallbackValue cv = mHistory.poll(TIMEOUT_MS, c -> true);
+ assertNotNull("No onStopTetheringFailed after " + TIMEOUT_MS + " ms", cv);
+ assertTrue("Expect fail with error code " + expected + ", but received: " + cv,
+ (cv instanceof CallbackValue.OnStopTetheringFailed) && (cv.error == expected));
+ }
+ }
+
private static boolean isRegexMatch(final String[] ifaceRegexs, String iface) {
if (ifaceRegexs == null) fail("ifaceRegexs should not be null");
@@ -574,6 +640,22 @@
expectSoftApDisabled();
}
+ /**
+ * Calls {@link TetheringManager#stopTethering(TetheringRequest, Executor,
+ * TetheringManager.StopTetheringCallback)} and verifies if it succeeded or failed.
+ */
+ public void stopTethering(final TetheringRequest request, boolean expectSuccess) {
+ final StopTetheringCallback stopTetheringCallback = new StopTetheringCallback();
+ runAsShell(TETHER_PRIVILEGED, () -> {
+ mTm.stopTethering(request, c -> c.run() /* executor */, stopTetheringCallback);
+ if (expectSuccess) {
+ stopTetheringCallback.verifyStopTetheringSucceeded();
+ } else {
+ stopTetheringCallback.expectStopTetheringFailed(TETHER_ERROR_UNKNOWN_REQUEST);
+ }
+ });
+ }
+
public void stopAllTethering() {
final TestTetheringEventCallback callback = registerTetheringEventCallback();
try {
diff --git a/tests/cts/tethering/Android.bp b/tests/cts/tethering/Android.bp
index d167836..a1e0797 100644
--- a/tests/cts/tethering/Android.bp
+++ b/tests/cts/tethering/Android.bp
@@ -55,34 +55,6 @@
host_required: ["net-tests-utils-host-common"],
}
-// Tethering CTS tests that target the latest released SDK. These tests can be installed on release
-// devices which has equal or lowner sdk version than target sdk and are useful for qualifying
-// mainline modules on release devices.
-android_test {
- name: "CtsTetheringTestLatestSdk",
- defaults: [
- "ConnectivityTestsLatestSdkDefaults",
- "CtsTetheringTestDefaults",
- ],
-
- min_sdk_version: "30",
-
- static_libs: [
- "TetheringIntegrationTestsLatestSdkLib",
- ],
-
- test_suites: [
- "general-tests",
- "mts-tethering",
- ],
-
- test_config_template: "AndroidTestTemplate.xml",
-
- // Include both the 32 and 64 bit versions
- compile_multilib: "both",
- jarjar_rules: ":NetworkStackJarJarRules",
-}
-
// Tethering CTS tests for development and release. These tests always target the platform SDK
// version, and are subject to all the restrictions appropriate to that version. Before SDK
// finalization, these tests have a min_sdk_version of 10000, but they can still be installed on
diff --git a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
index d103f75..9e49926 100644
--- a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
+++ b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
@@ -101,7 +101,6 @@
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
@@ -458,14 +457,26 @@
@Test
public void testStopTetheringRequest() throws Exception {
- TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI).build();
- Executor executor = Runnable::run;
- TetheringManager.StopTetheringCallback callback =
- new TetheringManager.StopTetheringCallback() {};
+ assumeTrue(isTetheringWithSoftApConfigEnabled());
+ final TestTetheringEventCallback tetherEventCallback =
+ mCtsTetheringUtils.registerTetheringEventCallback();
try {
- mTM.stopTethering(request, executor, callback);
- fail("stopTethering should throw UnsupportedOperationException");
- } catch (UnsupportedOperationException expect) { }
+ tetherEventCallback.assumeWifiTetheringSupported(mContext);
+
+ // stopTethering without any tethering active should fail.
+ TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI).build();
+ mCtsTetheringUtils.stopTethering(request, false /* expectSuccess */);
+
+ // Start wifi tethering
+ mCtsTetheringUtils.startWifiTethering(tetherEventCallback);
+
+ // stopTethering should succeed now that there's a request.
+ mCtsTetheringUtils.stopTethering(request, true /* expectSuccess */);
+ tetherEventCallback.expectNoTetheringActive();
+ } finally {
+ mCtsTetheringUtils.stopAllTethering();
+ mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
+ }
}
private boolean isTetheringWithSoftApConfigEnabled() {
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 06bdca6..437eb81 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
@@ -16,6 +16,7 @@
package com.android.server.net.integrationtests
+import android.Manifest.permission
import android.app.usage.NetworkStatsManager
import android.content.ComponentName
import android.content.Context
@@ -54,18 +55,21 @@
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
import com.android.testutils.DeviceInfoUtils
import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.runAsShell
import com.android.testutils.tryTest
import java.util.function.BiConsumer
import java.util.function.Consumer
@@ -208,7 +212,9 @@
networkStackClient = TestNetworkStackClient(realContext)
networkStackClient.start()
- service = TestConnectivityService(TestDependencies())
+ service = runAsShell(permission.OBSERVE_GRANT_REVOKE_PERMISSIONS) {
+ TestConnectivityService(TestDependencies())
+ }
cm = ConnectivityManager(context, service)
context.addMockSystemService(Context.CONNECTIVITY_SERVICE, cm)
context.addMockSystemService(Context.NETWORK_STATS_SERVICE, statsManager)
@@ -217,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
@@ -268,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/native/connectivity_native_test/Android.bp b/tests/native/connectivity_native_test/Android.bp
index 39a08fa..02ac3c5 100644
--- a/tests/native/connectivity_native_test/Android.bp
+++ b/tests/native/connectivity_native_test/Android.bp
@@ -24,7 +24,6 @@
"libcom.android.tethering.connectivity_native",
"liblog",
"libnetutils",
- "libprocessgroup",
],
static_libs: [
"connectivity_native_aidl_interface-lateststable-ndk",
diff --git a/tests/unit/java/android/net/ConnectivityManagerTest.java b/tests/unit/java/android/net/ConnectivityManagerTest.java
index b415382..9a77c89 100644
--- a/tests/unit/java/android/net/ConnectivityManagerTest.java
+++ b/tests/unit/java/android/net/ConnectivityManagerTest.java
@@ -44,7 +44,6 @@
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.nullable;
@@ -66,7 +65,6 @@
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.net.ConnectivityManager.NetworkCallback;
-import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.Handler;
@@ -671,12 +669,4 @@
// No callbacks overridden -> do not use the optimization
eq(~0));
}
-
- @Test
- public void testLegacyTetherApisThrowUnsupportedOperationExceptionAfterV() {
- assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM);
- final ConnectivityManager manager = new ConnectivityManager(mCtx, mService);
- assertThrows(UnsupportedOperationException.class, () -> manager.tether("iface"));
- assertThrows(UnsupportedOperationException.class, () -> manager.untether("iface"));
- }
}
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index c1c15ca..caf1765 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -71,6 +71,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
@@ -99,6 +100,7 @@
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.IBpfMap;
+import com.android.net.module.util.Struct.Bool;
import com.android.net.module.util.Struct.S32;
import com.android.net.module.util.Struct.U32;
import com.android.net.module.util.Struct.U8;
@@ -106,6 +108,7 @@
import com.android.net.module.util.bpf.CookieTagMapValue;
import com.android.net.module.util.bpf.IngressDiscardKey;
import com.android.net.module.util.bpf.IngressDiscardValue;
+import com.android.net.module.util.bpf.LocalNetAccessKey;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
@@ -171,6 +174,10 @@
private final IBpfMap<S32, UidOwnerValue> mUidOwnerMap =
new TestBpfMap<>(S32.class, UidOwnerValue.class);
private final IBpfMap<S32, U8> mUidPermissionMap = new TestBpfMap<>(S32.class, U8.class);
+ private final IBpfMap<U32, Bool> mLocalNetBlockedUidMap =
+ new TestBpfMap<>(U32.class, Bool.class);
+ private final IBpfMap<LocalNetAccessKey, Bool> mLocalNetAccessMap =
+ new TestBpfMap<>(LocalNetAccessKey.class, Bool.class);
private final IBpfMap<CookieTagMapKey, CookieTagMapValue> mCookieTagMap =
spy(new TestBpfMap<>(CookieTagMapKey.class, CookieTagMapValue.class));
private final IBpfMap<S32, U8> mDataSaverEnabledMap = new TestBpfMap<>(S32.class, U8.class);
@@ -189,6 +196,8 @@
CURRENT_STATS_MAP_CONFIGURATION_KEY, new U32(STATS_SELECT_MAP_A));
BpfNetMaps.setUidOwnerMapForTest(mUidOwnerMap);
BpfNetMaps.setUidPermissionMapForTest(mUidPermissionMap);
+ BpfNetMaps.setLocalNetAccessMapForTest(mLocalNetAccessMap);
+ BpfNetMaps.setLocalNetBlockedUidMapForTest(mLocalNetBlockedUidMap);
BpfNetMaps.setCookieTagMapForTest(mCookieTagMap);
BpfNetMaps.setDataSaverEnabledMapForTest(mDataSaverEnabledMap);
mDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, new U8(DATA_SAVER_DISABLED));
@@ -235,6 +244,225 @@
}
@Test
+ @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testAddLocalNetAccessBeforeV() {
+ assertThrows(UnsupportedOperationException.class, () ->
+ mBpfNetMaps.addLocalNetAccess(0, TEST_IF_NAME, Inet6Address.ANY, 0, 0, true));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testAddLocalNetAccessAfterV() throws Exception {
+ assertTrue(mLocalNetAccessMap.isEmpty());
+
+ mBpfNetMaps.addLocalNetAccess(160, TEST_IF_NAME,
+ Inet4Address.getByName("196.68.0.0"), 0, 0, true);
+
+ assertNotNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("196.68.0.0"), 0, 0)));
+ assertNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("100.68.0.0"), 0, 0)));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testAddLocalNetAccessWithNullInterfaceAfterV() throws Exception {
+ assertTrue(mLocalNetAccessMap.isEmpty());
+
+ mBpfNetMaps.addLocalNetAccess(160, null,
+ Inet4Address.getByName("196.68.0.0"), 0, 0, true);
+
+ // As we tried to add null interface, it would be skipped and map should be empty.
+ assertTrue(mLocalNetAccessMap.isEmpty());
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testAddLocalNetAccessAfterVWithIncorrectInterface() throws Exception {
+ assertTrue(mLocalNetAccessMap.isEmpty());
+
+ // wlan2 is an incorrect interface
+ mBpfNetMaps.addLocalNetAccess(160, "wlan2",
+ Inet4Address.getByName("196.68.0.0"), 0, 0, true);
+
+ // As we tried to add incorrect interface, it would be skipped and map should be empty.
+ assertTrue(mLocalNetAccessMap.isEmpty());
+ }
+
+ @Test
+ @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testGetLocalNetAccessBeforeV() {
+ assertThrows(UnsupportedOperationException.class, () ->
+ mBpfNetMaps.getLocalNetAccess(0, TEST_IF_NAME, Inet6Address.ANY, 0, 0));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testGetLocalNetAccessAfterV() throws Exception {
+ assertTrue(mLocalNetAccessMap.isEmpty());
+
+ mLocalNetAccessMap.updateEntry(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("196.68.0.0"), 0, 0),
+ new Bool(false));
+
+ assertNotNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("196.68.0.0"), 0, 0)));
+
+ assertFalse(mBpfNetMaps.getLocalNetAccess(160, TEST_IF_NAME,
+ Inet4Address.getByName("196.68.0.0"), 0, 0));
+ assertTrue(mBpfNetMaps.getLocalNetAccess(160, TEST_IF_NAME,
+ Inet4Address.getByName("100.68.0.0"), 0, 0));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testGetLocalNetAccessWithNullInterfaceAfterV() throws Exception {
+ assertTrue(mBpfNetMaps.getLocalNetAccess(160, null,
+ Inet4Address.getByName("100.68.0.0"), 0, 0));
+ }
+
+ @Test
+ @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testRemoveLocalNetAccessBeforeV() {
+ assertThrows(UnsupportedOperationException.class, () ->
+ mBpfNetMaps.removeLocalNetAccess(0, TEST_IF_NAME, Inet6Address.ANY, 0, 0));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testRemoveLocalNetAccessAfterV() throws Exception {
+ assertTrue(mLocalNetAccessMap.isEmpty());
+
+ mBpfNetMaps.addLocalNetAccess(160, TEST_IF_NAME,
+ Inet4Address.getByName("196.68.0.0"), 0, 0, true);
+
+ assertNotNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("196.68.0.0"), 0, 0)));
+ assertNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("100.68.0.0"), 0, 0)));
+
+ mBpfNetMaps.removeLocalNetAccess(160, TEST_IF_NAME,
+ Inet4Address.getByName("196.68.0.0"), 0, 0);
+ assertNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("196.68.0.0"), 0, 0)));
+ assertNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("100.68.0.0"), 0, 0)));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testRemoveLocalNetAccessAfterVWithIncorrectInterface() throws Exception {
+ assertTrue(mLocalNetAccessMap.isEmpty());
+
+ mBpfNetMaps.addLocalNetAccess(160, TEST_IF_NAME,
+ Inet4Address.getByName("196.68.0.0"), 0, 0, true);
+
+ assertNotNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("196.68.0.0"), 0, 0)));
+ assertNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("100.68.0.0"), 0, 0)));
+
+ mBpfNetMaps.removeLocalNetAccess(160, "wlan2",
+ Inet4Address.getByName("196.68.0.0"), 0, 0);
+ assertNotNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("196.68.0.0"), 0, 0)));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testRemoveLocalNetAccessAfterVWithNullInterface() throws Exception {
+ assertTrue(mLocalNetAccessMap.isEmpty());
+
+ mBpfNetMaps.addLocalNetAccess(160, TEST_IF_NAME,
+ Inet4Address.getByName("196.68.0.0"), 0, 0, true);
+
+ assertNotNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("196.68.0.0"), 0, 0)));
+ assertNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("100.68.0.0"), 0, 0)));
+
+ mBpfNetMaps.removeLocalNetAccess(160, null,
+ Inet4Address.getByName("196.68.0.0"), 0, 0);
+ assertNotNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("196.68.0.0"), 0, 0)));
+ }
+
+ @Test
+ @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testAddUidToLocalNetBlockMapBeforeV() {
+ assertThrows(UnsupportedOperationException.class, () ->
+ mBpfNetMaps.addUidToLocalNetBlockMap(0));
+ }
+
+ @Test
+ @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testIsUidBlockedFromUsingLocalNetworkBeforeV() {
+ assertThrows(UnsupportedOperationException.class, () ->
+ mBpfNetMaps.isUidBlockedFromUsingLocalNetwork(0));
+ }
+
+ @Test
+ @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testRemoveUidFromLocalNetBlockMapBeforeV() {
+ assertThrows(UnsupportedOperationException.class, () ->
+ mBpfNetMaps.removeUidFromLocalNetBlockMap(0));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testAddUidFromLocalNetBlockMapAfterV() throws Exception {
+ final int uid0 = TEST_UIDS[0];
+ final int uid1 = TEST_UIDS[1];
+
+ assertTrue(mLocalNetAccessMap.isEmpty());
+
+ mBpfNetMaps.addUidToLocalNetBlockMap(uid0);
+ assertTrue(mLocalNetBlockedUidMap.getValue(new U32(uid0)).val);
+ assertNull(mLocalNetBlockedUidMap.getValue(new U32(uid1)));
+
+ mBpfNetMaps.addUidToLocalNetBlockMap(uid1);
+ assertTrue(mLocalNetBlockedUidMap.getValue(new U32(uid1)).val);
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testIsUidBlockedFromUsingLocalNetworkAfterV() throws Exception {
+ final int uid0 = TEST_UIDS[0];
+ final int uid1 = TEST_UIDS[1];
+
+ assertTrue(mLocalNetAccessMap.isEmpty());
+
+ mLocalNetBlockedUidMap.updateEntry(new U32(uid0), new Bool(true));
+ assertTrue(mBpfNetMaps.isUidBlockedFromUsingLocalNetwork(uid0));
+ assertFalse(mBpfNetMaps.isUidBlockedFromUsingLocalNetwork(uid1));
+
+ mLocalNetBlockedUidMap.updateEntry(new U32(uid1), new Bool(true));
+ assertTrue(mBpfNetMaps.isUidBlockedFromUsingLocalNetwork(uid1));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testRemoveUidFromLocalNetBlockMapAfterV() throws Exception {
+ final int uid0 = TEST_UIDS[0];
+ final int uid1 = TEST_UIDS[1];
+
+ assertTrue(mLocalNetAccessMap.isEmpty());
+
+ mLocalNetBlockedUidMap.updateEntry(new U32(uid0), new Bool(true));
+ mLocalNetBlockedUidMap.updateEntry(new U32(uid1), new Bool(true));
+
+ assertTrue(mLocalNetBlockedUidMap.getValue(new U32(uid0)).val);
+ assertTrue(mLocalNetBlockedUidMap.getValue(new U32(uid1)).val);
+
+ mBpfNetMaps.removeUidFromLocalNetBlockMap(uid0);
+ assertNull(mLocalNetBlockedUidMap.getValue(new U32(uid0)));
+ assertTrue(mLocalNetBlockedUidMap.getValue(new U32(uid1)).val);
+
+ mBpfNetMaps.removeUidFromLocalNetBlockMap(uid1);
+ assertNull(mLocalNetBlockedUidMap.getValue(new U32(uid1)));
+ }
+
+ @Test
@IgnoreUpTo(Build.VERSION_CODES.S_V2)
public void testIsChainEnabled() throws Exception {
doTestIsChainEnabled(FIREWALL_CHAIN_DOZABLE);
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index f7d7c87..19a41d8 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -163,10 +163,7 @@
import static android.telephony.DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH;
import static android.telephony.DataConnectionRealTimeInfo.DC_POWER_STATE_LOW;
-import static com.android.server.ConnectivityService.ALLOW_SATALLITE_NETWORK_FALLBACK;
import static com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME;
-import static com.android.server.ConnectivityService.ALLOW_SYSUI_CONNECTIVITY_REPORTS;
-import static com.android.server.ConnectivityService.KEY_DESTROY_FROZEN_SOCKETS_VERSION;
import static com.android.server.ConnectivityService.MAX_NETWORK_REQUESTS_PER_SYSTEM_UID;
import static com.android.server.ConnectivityService.PREFERENCE_ORDER_MOBILE_DATA_PREFERERRED;
import static com.android.server.ConnectivityService.PREFERENCE_ORDER_OEM;
@@ -177,9 +174,6 @@
import static com.android.server.ConnectivityServiceTestUtils.transportToLegacyType;
import static com.android.server.NetworkAgentWrapper.CallbackType.OnQosCallbackRegister;
import static com.android.server.NetworkAgentWrapper.CallbackType.OnQosCallbackUnregister;
-import static com.android.server.connectivity.ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN;
-import static com.android.server.connectivity.ConnectivityFlags.DELAY_DESTROY_SOCKETS;
-import static com.android.server.connectivity.ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING;
import static com.android.testutils.Cleanup.testAndCleanup;
import static com.android.testutils.ConcurrentUtils.await;
import static com.android.testutils.ConcurrentUtils.durationOf;
@@ -412,6 +406,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 +420,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 +589,7 @@
private MockContext mServiceContext;
private HandlerThread mCsHandlerThread;
private ConnectivityServiceDependencies mDeps;
+ private PermissionMonitorDependencies mPermDeps;
private AutomaticOnOffKeepaliveTrackerDependencies mAutoOnOffKeepaliveDependencies;
private ConnectivityService mService;
private WrappedConnectivityManager mCm;
@@ -1920,6 +1917,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 +1930,7 @@
mMockDnsResolver,
mock(IpConnectivityLog.class),
mMockNetd,
- mDeps);
+ mDeps, mPermDeps);
mService.mLingerDelayMs = TEST_LINGER_DELAY_MS;
mService.mNascentDelayMs = TEST_NASCENT_DELAY_MS;
@@ -2181,28 +2179,30 @@
case ConnectivityFlags.REQUEST_RESTRICTED_WIFI:
case ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS:
case ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS:
- case KEY_DESTROY_FROZEN_SOCKETS_VERSION:
+ case ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN:
+ case ConnectivityService.KEY_DESTROY_FROZEN_SOCKETS_VERSION:
return true;
default:
- return super.isFeatureEnabled(context, name);
+ // This is a unit test and must never depend on actual device flag values.
+ throw new UnsupportedOperationException("Unknown flag " + name
+ + ", update this test");
}
}
@Override
public boolean isFeatureNotChickenedOut(Context context, String name) {
switch (name) {
- case ALLOW_SYSUI_CONNECTIVITY_REPORTS:
- return true;
- case ALLOW_SATALLITE_NETWORK_FALLBACK:
- return true;
- case INGRESS_TO_VPN_ADDRESS_FILTERING:
- return true;
- case BACKGROUND_FIREWALL_CHAIN:
- return true;
- case DELAY_DESTROY_SOCKETS:
+ case ConnectivityService.ALLOW_SYSUI_CONNECTIVITY_REPORTS:
+ case ConnectivityService.ALLOW_SATALLITE_NETWORK_FALLBACK:
+ case ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING:
+ case ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN:
+ case ConnectivityFlags.DELAY_DESTROY_SOCKETS:
+ case ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS:
+ case ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS:
return true;
default:
- return super.isFeatureNotChickenedOut(context, name);
+ throw new UnsupportedOperationException("Unknown flag " + name
+ + ", update this test");
}
}
@@ -2381,6 +2381,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
@@ -2431,6 +2443,10 @@
@After
public void tearDown() throws Exception {
+ // Don't attempt to tear down if setUp didn't even get as far as creating the service.
+ // Otherwise, exceptions here will mask the actual exception in setUp, making failures
+ // harder to diagnose.
+ if (mService == null) return;
unregisterDefaultNetworkCallbacks();
maybeTearDownEnterpriseNetwork();
setAlwaysOnNetworks(false);
diff --git a/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt b/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt
deleted file mode 100644
index 5a7515e..0000000
--- a/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt
+++ /dev/null
@@ -1,216 +0,0 @@
-/*
- * 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.content.Context
-import android.content.pm.PackageManager
-import android.content.pm.PackageManager.FEATURE_BLUETOOTH_LE
-import android.net.ConnectivityManager
-import android.net.ConnectivityManager.TYPE_NONE
-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
-import android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH
-import android.net.NetworkProvider
-import android.net.NetworkProvider.NetworkOfferCallback
-import android.net.NetworkRequest
-import android.os.Build
-import android.os.Handler
-import android.os.HandlerThread
-import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.DevSdkIgnoreRunner
-import kotlin.test.assertTrue
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers.any
-import org.mockito.ArgumentMatchers.eq
-import org.mockito.Mock
-import org.mockito.Mockito.any
-import org.mockito.Mockito.clearInvocations
-import org.mockito.Mockito.doReturn
-import org.mockito.Mockito.never
-import org.mockito.Mockito.times
-import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-
-const val TAG = "L2capNetworkProviderTest"
-
-val RESERVATION_CAPS = NetworkCapabilities.Builder.withoutDefaultCapabilities()
- .addTransportType(TRANSPORT_BLUETOOTH)
- .build()
-
-val RESERVATION = NetworkRequest(
- NetworkCapabilities(RESERVATION_CAPS),
- TYPE_NONE,
- 42 /* rId */,
- NetworkRequest.Type.RESERVATION
-)
-
-@RunWith(DevSdkIgnoreRunner::class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
-class L2capNetworkProviderTest {
- @Mock private lateinit var context: Context
- @Mock private lateinit var deps: L2capNetworkProvider.Dependencies
- @Mock private lateinit var provider: NetworkProvider
- @Mock private lateinit var cm: ConnectivityManager
- @Mock private lateinit var pm: PackageManager
-
- private val handlerThread = HandlerThread("$TAG handler thread").apply { start() }
- private val handler = Handler(handlerThread.looper)
-
- @Before
- fun setUp() {
- MockitoAnnotations.initMocks(this)
- doReturn(provider).`when`(deps).getNetworkProvider(any(), any())
- doReturn(handlerThread).`when`(deps).getHandlerThread()
- doReturn(cm).`when`(context).getSystemService(eq(ConnectivityManager::class.java))
- doReturn(pm).`when`(context).getPackageManager()
- doReturn(true).`when`(pm).hasSystemFeature(FEATURE_BLUETOOTH_LE)
- }
-
- @After
- fun tearDown() {
- handlerThread.quitSafely()
- handlerThread.join()
- }
-
- @Test
- fun testNetworkProvider_registeredWhenSupported() {
- L2capNetworkProvider(deps, context).start()
- verify(cm).registerNetworkProvider(eq(provider))
- verify(provider).registerNetworkOffer(any(), any(), any(), any())
- }
-
- @Test
- fun testNetworkProvider_notRegisteredWhenNotSupported() {
- doReturn(false).`when`(pm).hasSystemFeature(FEATURE_BLUETOOTH_LE)
- L2capNetworkProvider(deps, context).start()
- verify(cm, never()).registerNetworkProvider(eq(provider))
- }
-
- fun doTestBlanketOfferIgnoresRequest(request: NetworkRequest) {
- clearInvocations(provider)
- L2capNetworkProvider(deps, context).start()
-
- val blanketOfferCaptor = ArgumentCaptor.forClass(NetworkOfferCallback::class.java)
- verify(provider).registerNetworkOffer(any(), any(), any(), blanketOfferCaptor.capture())
-
- blanketOfferCaptor.value.onNetworkNeeded(request)
- verify(provider).registerNetworkOffer(any(), any(), any(), any())
- }
-
- fun doTestBlanketOfferCreatesReservation(
- request: NetworkRequest,
- reservation: NetworkCapabilities
- ) {
- clearInvocations(provider)
- L2capNetworkProvider(deps, context).start()
-
- val blanketOfferCaptor = ArgumentCaptor.forClass(NetworkOfferCallback::class.java)
- verify(provider).registerNetworkOffer(any(), any(), any(), blanketOfferCaptor.capture())
-
- blanketOfferCaptor.value.onNetworkNeeded(request)
-
- val capsCaptor = ArgumentCaptor.forClass(NetworkCapabilities::class.java)
- verify(provider, times(2)).registerNetworkOffer(any(), capsCaptor.capture(), any(), any())
-
- assertTrue(reservation.satisfiedByNetworkCapabilities(capsCaptor.value))
- }
-
- @Test
- fun testBlanketOffer_reservationWithoutSpecifier() {
- val caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
- .addTransportType(TRANSPORT_BLUETOOTH)
- .build()
- val nr = NetworkRequest(caps, TYPE_NONE, 42 /* rId */, NetworkRequest.Type.RESERVATION)
-
- doTestBlanketOfferIgnoresRequest(nr)
- }
-
- @Test
- fun testBlanketOffer_reservationWithCorrectSpecifier() {
- var specifier = L2capNetworkSpecifier.Builder()
- .setRole(ROLE_SERVER)
- .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
- .build()
- var caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
- .addTransportType(TRANSPORT_BLUETOOTH)
- .setNetworkSpecifier(specifier)
- .build()
- var nr = NetworkRequest(caps, TYPE_NONE, 42 /* rId */, NetworkRequest.Type.RESERVATION)
- doTestBlanketOfferCreatesReservation(nr, caps)
-
- specifier = L2capNetworkSpecifier.Builder()
- .setRole(ROLE_SERVER)
- .setHeaderCompression(HEADER_COMPRESSION_NONE)
- .build()
- caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
- .addTransportType(TRANSPORT_BLUETOOTH)
- .setNetworkSpecifier(specifier)
- .build()
- nr = NetworkRequest(caps, TYPE_NONE, 43 /* rId */, NetworkRequest.Type.RESERVATION)
- doTestBlanketOfferCreatesReservation(nr, caps)
- }
-
- @Test
- fun testBlanketOffer_reservationWithIncorrectSpecifier() {
- var specifier = L2capNetworkSpecifier.Builder().build()
- var caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
- .addTransportType(TRANSPORT_BLUETOOTH)
- .setNetworkSpecifier(specifier)
- .build()
- var nr = NetworkRequest(caps, TYPE_NONE, 42 /* rId */, NetworkRequest.Type.RESERVATION)
- doTestBlanketOfferIgnoresRequest(nr)
-
- specifier = L2capNetworkSpecifier.Builder()
- .setRole(ROLE_SERVER)
- .build()
- caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
- .addTransportType(TRANSPORT_BLUETOOTH)
- .setNetworkSpecifier(specifier)
- .build()
- nr = NetworkRequest(caps, TYPE_NONE, 44 /* rId */, NetworkRequest.Type.RESERVATION)
- doTestBlanketOfferIgnoresRequest(nr)
-
- specifier = L2capNetworkSpecifier.Builder()
- .setRole(ROLE_SERVER)
- .setHeaderCompression(HEADER_COMPRESSION_NONE)
- .setPsm(0x81)
- .build()
- caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
- .addTransportType(TRANSPORT_BLUETOOTH)
- .setNetworkSpecifier(specifier)
- .build()
- nr = NetworkRequest(caps, TYPE_NONE, 45 /* rId */, NetworkRequest.Type.RESERVATION)
- doTestBlanketOfferIgnoresRequest(nr)
-
- specifier = L2capNetworkSpecifier.Builder()
- .setHeaderCompression(HEADER_COMPRESSION_NONE)
- .build()
- caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
- .addTransportType(TRANSPORT_BLUETOOTH)
- .setNetworkSpecifier(specifier)
- .build()
- nr = NetworkRequest(caps, TYPE_NONE, 47 /* rId */, NetworkRequest.Type.RESERVATION)
- doTestBlanketOfferIgnoresRequest(nr)
- }
-}
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 5bde31a..ec9c6b0 100644
--- a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
@@ -21,7 +21,9 @@
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;
import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_OEM;
import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_PRODUCT;
@@ -30,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;
@@ -38,15 +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;
@@ -66,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;
@@ -83,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;
@@ -100,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;
@@ -119,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;
@@ -160,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;
@@ -181,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))
@@ -293,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() {
@@ -340,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) {
@@ -352,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
@@ -583,6 +621,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testHasUseBackgroundNetworksPermission() throws Exception {
assertFalse(mPermissionMonitor.hasUseBackgroundNetworksPermission(SYSTEM_UID));
assertBackgroundPermission(false, SYSTEM_PACKAGE1, SYSTEM_UID);
@@ -604,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 {
@@ -616,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) {
@@ -640,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 {
@@ -723,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.
@@ -812,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(
@@ -858,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 */);
@@ -895,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,
@@ -936,6 +1048,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testLockdownUidFilteringWithLockdownEnableDisableWithMultiAdd() {
doReturn(List.of(
buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
@@ -977,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),
@@ -1037,6 +1151,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testLockdownUidFilteringWithLockdownEnableDisableWithDuplicates() {
doReturn(List.of(
buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
@@ -1071,6 +1186,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testLockdownUidFilteringWithInstallAndUnInstall() {
doReturn(List.of(
buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
@@ -1107,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));
}
}
@@ -1163,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);
@@ -1171,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);
@@ -1183,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);
@@ -1192,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);
@@ -1205,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);
@@ -1215,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);
@@ -1235,8 +1410,10 @@
// Use the real context as this test must ensure the *real* system package holds the
// necessary permission.
final Context realContext = InstrumentationRegistry.getContext();
- final PermissionMonitor monitor = new PermissionMonitor(
- realContext, mNetdService, mBpfNetMaps, mHandlerThread);
+ final PermissionMonitor monitor = runAsShell(
+ OBSERVE_GRANT_REVOKE_PERMISSIONS,
+ () -> new PermissionMonitor(realContext, mNetdService, mBpfNetMaps, mHandlerThread)
+ );
final PackageManager manager = realContext.getPackageManager();
final PackageInfo systemInfo = manager.getPackageInfo(REAL_SYSTEM_PACKAGE_NAME,
GET_PERMISSIONS | MATCH_ANY_USER);
@@ -1244,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 });
@@ -1283,6 +1461,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testIntentReceiver() throws Exception {
startMonitoring();
final BroadcastReceiver receiver = expectBroadcastReceiver(
@@ -1321,6 +1500,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testUidsAllowedOnRestrictedNetworksChanged() throws Exception {
startMonitoring();
final ContentObserver contentObserver = expectRegisterContentObserver(
@@ -1353,6 +1533,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testUidsAllowedOnRestrictedNetworksChangedWithSharedUid() throws Exception {
startMonitoring();
final ContentObserver contentObserver = expectRegisterContentObserver(
@@ -1386,6 +1567,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testUidsAllowedOnRestrictedNetworksChangedWithMultipleUsers() throws Exception {
startMonitoring();
final ContentObserver contentObserver = expectRegisterContentObserver(
@@ -1440,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.
@@ -1471,6 +1654,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testOnExternalApplicationsAvailable_AppsNotRegisteredOnStartMonitoring()
throws Exception {
startMonitoring();
@@ -1498,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
@@ -1524,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
@@ -1566,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.
@@ -1598,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);
@@ -1606,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();
@@ -1646,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);
@@ -1716,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/connectivity/mdns/DiscoveryExecutorTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/DiscoveryExecutorTest.kt
new file mode 100644
index 0000000..51539a0
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/DiscoveryExecutorTest.kt
@@ -0,0 +1,110 @@
+/*
+ * 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.connectivity.mdns
+
+import android.os.Build
+import android.os.HandlerThread
+import android.testing.TestableLooper
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import org.junit.After
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val DEFAULT_TIMEOUT = 2000L
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
+class DiscoveryExecutorTest {
+ private val thread = HandlerThread(DiscoveryExecutorTest::class.simpleName).apply { start() }
+
+ @After
+ fun tearDown() {
+ thread.quitSafely()
+ thread.join()
+ }
+
+ @Test
+ fun testCheckAndRunOnHandlerThread() {
+ val testableLooper = TestableLooper(thread.looper)
+ val executor = DiscoveryExecutor(testableLooper.looper)
+ try {
+ val future = CompletableFuture<Boolean>()
+ executor.checkAndRunOnHandlerThread { future.complete(true) }
+ testableLooper.processAllMessages()
+ assertTrue(future.get(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS))
+ } finally {
+ testableLooper.destroy()
+ }
+
+ // Create a DiscoveryExecutor with the null defaultLooper and verify the task can execute
+ // normally.
+ val executor2 = DiscoveryExecutor(null /* defaultLooper */)
+ val future2 = CompletableFuture<Boolean>()
+ executor2.checkAndRunOnHandlerThread { future2.complete(true) }
+ assertTrue(future2.get(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS))
+ executor2.shutDown()
+ }
+
+ @Test
+ fun testExecute() {
+ val testableLooper = TestableLooper(thread.looper)
+ val executor = DiscoveryExecutor(testableLooper.looper)
+ try {
+ val future = CompletableFuture<Boolean>()
+ executor.execute { future.complete(true) }
+ assertFalse(future.isDone)
+ testableLooper.processAllMessages()
+ assertTrue(future.get(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS))
+ } finally {
+ testableLooper.destroy()
+ }
+ }
+
+ @Test
+ fun testExecuteDelayed() {
+ val testableLooper = TestableLooper(thread.looper)
+ val executor = DiscoveryExecutor(testableLooper.looper)
+ try {
+ // Verify the executeDelayed method
+ val future = CompletableFuture<Boolean>()
+ // Schedule a task with 999 ms delay
+ executor.executeDelayed({ future.complete(true) }, 999L)
+ testableLooper.processAllMessages()
+ assertFalse(future.isDone)
+
+ // 500 ms have elapsed but do not exceed the target time (999 ms)
+ // The function should not be executed.
+ testableLooper.moveTimeForward(500L)
+ testableLooper.processAllMessages()
+ assertFalse(future.isDone)
+
+ // 500 ms have elapsed again and have exceeded the target time (999 ms).
+ // The function should be executed.
+ testableLooper.moveTimeForward(500L)
+ testableLooper.processAllMessages()
+ assertTrue(future.get(500L, TimeUnit.MILLISECONDS))
+ } finally {
+ testableLooper.destroy()
+ }
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
index ba62114..087617a 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
@@ -92,6 +92,20 @@
network = TEST_NETWORK_1
}
+private val GOOGLECAST_SERVICE = NsdServiceInfo("TestServiceName", "_googlecast._tcp").apply {
+ subtypes = setOf(TEST_SUBTYPE)
+ port = 12345
+ hostAddresses = listOf(TEST_ADDR)
+ network = TEST_NETWORK_1
+}
+
+private val GOOGLEZONE_SERVICE = NsdServiceInfo("TestServiceName", "_GOOglezone._tcp").apply {
+ subtypes = setOf(TEST_SUBTYPE)
+ port = 12345
+ hostAddresses = listOf(TEST_ADDR)
+ network = TEST_NETWORK_1
+}
+
private val SERVICE_1_SUBTYPE = NsdServiceInfo("TestServiceName", "_advertisertest._tcp").apply {
subtypes = setOf(TEST_SUBTYPE)
port = 12345
@@ -143,6 +157,15 @@
OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
)
+private val OFFLOAD_SERVICE_INFO_GOOGLECAST = OffloadServiceInfo(
+ OffloadServiceInfo.Key("TestServiceName", "_googlecast._tcp"),
+ listOf(),
+ "Android_test.local",
+ TEST_OFFLOAD_PACKET1,
+ Int.MAX_VALUE,
+ OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
+)
+
private val OFFLOAD_SERVICEINFO_NO_SUBTYPE = OffloadServiceInfo(
OffloadServiceInfo.Key("TestServiceName", "_advertisertest._tcp"),
listOf(),
@@ -167,6 +190,10 @@
"5:_otherprioritytest._tcp"
)
+private val SERVICES_DENY_LIST = arrayOf(
+ "_googlezone._tcp",
+)
+
@RunWith(DevSdkIgnoreRunner::class)
@IgnoreUpTo(Build.VERSION_CODES.S_V2)
class MdnsAdvertiserTest {
@@ -189,11 +216,25 @@
fun setUp() {
thread.start()
doReturn(TEST_HOSTNAME).`when`(mockDeps).generateHostname(anyBoolean())
- doReturn(mockInterfaceAdvertiser1).`when`(mockDeps).makeAdvertiser(eq(mockSocket1),
- any(), any(), any(), any(), any(), any(), any()
+ doReturn(mockInterfaceAdvertiser1).`when`(mockDeps).makeAdvertiser(
+ eq(mockSocket1),
+ any(),
+ any(),
+ any(),
+ any(),
+ any(),
+ any(),
+ any()
)
- doReturn(mockInterfaceAdvertiser2).`when`(mockDeps).makeAdvertiser(eq(mockSocket2),
- any(), any(), any(), any(), any(), any(), any()
+ doReturn(mockInterfaceAdvertiser2).`when`(mockDeps).makeAdvertiser(
+ eq(mockSocket2),
+ any(),
+ any(),
+ any(),
+ any(),
+ any(),
+ any(),
+ any()
)
doReturn(true).`when`(mockInterfaceAdvertiser1).isProbing(anyInt())
doReturn(true).`when`(mockInterfaceAdvertiser2).isProbing(anyInt())
@@ -202,16 +243,24 @@
doReturn(TEST_INTERFACE1).`when`(mockInterfaceAdvertiser1).socketInterfaceName
doReturn(TEST_INTERFACE2).`when`(mockInterfaceAdvertiser2).socketInterfaceName
doReturn(TEST_OFFLOAD_PACKET1).`when`(mockInterfaceAdvertiser1).getRawOffloadPayload(
- SERVICE_ID_1)
+ SERVICE_ID_1
+ )
doReturn(TEST_OFFLOAD_PACKET1).`when`(mockInterfaceAdvertiser1).getRawOffloadPayload(
- SERVICE_ID_2)
+ SERVICE_ID_2
+ )
doReturn(TEST_OFFLOAD_PACKET1).`when`(mockInterfaceAdvertiser1).getRawOffloadPayload(
- SERVICE_ID_3)
+ SERVICE_ID_3
+ )
doReturn(TEST_OFFLOAD_PACKET1).`when`(mockInterfaceAdvertiser2).getRawOffloadPayload(
- SERVICE_ID_1)
+ SERVICE_ID_1
+ )
doReturn(resources).`when`(context).getResources()
doReturn(SERVICES_PRIORITY_LIST).`when`(resources).getStringArray(
- R.array.config_nsdOffloadServicesPriority)
+ R.array.config_nsdOffloadServicesPriority
+ )
+ doReturn(SERVICES_DENY_LIST).`when`(resources).getStringArray(
+ R.array.config_nsdOffloadServicesDenyList
+ )
ConnectivityResources.setResourcesContextForTest(context)
}
@@ -232,8 +281,12 @@
fun testAddService_OneNetwork() {
val advertiser =
MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
- postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
- DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
+ postSync { advertiser.addOrUpdateService(
+ SERVICE_ID_1,
+ SERVICE_1,
+ DEFAULT_ADVERTISING_OPTION,
+ TEST_CLIENT_UID_1
+ ) }
val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(TEST_NETWORK_1), socketCbCaptor.capture())
@@ -255,7 +308,9 @@
doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
postSync { intAdvCbCaptor.value.onServiceProbingSucceeded(
- mockInterfaceAdvertiser1, SERVICE_ID_1) }
+ mockInterfaceAdvertiser1,
+ SERVICE_ID_1
+ ) }
verify(cb).onRegisterServiceSucceeded(eq(SERVICE_ID_1), argThat { it.matches(SERVICE_1) })
verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICEINFO_NO_SUBTYPE))
@@ -296,12 +351,18 @@
fun testAddService_AllNetworksWithSubType() {
val advertiser =
MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
- postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE_SUBTYPE,
- DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
+ postSync { advertiser.addOrUpdateService(
+ SERVICE_ID_1,
+ ALL_NETWORKS_SERVICE_SUBTYPE,
+ DEFAULT_ADVERTISING_OPTION,
+ TEST_CLIENT_UID_1
+ ) }
val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
- verify(socketProvider).requestSocket(eq(ALL_NETWORKS_SERVICE_SUBTYPE.network),
- socketCbCaptor.capture())
+ verify(socketProvider).requestSocket(
+ eq(ALL_NETWORKS_SERVICE_SUBTYPE.network),
+ socketCbCaptor.capture()
+ )
val socketCb = socketCbCaptor.value
postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
@@ -309,30 +370,56 @@
val intAdvCbCaptor1 = ArgumentCaptor.forClass(MdnsInterfaceAdvertiser.Callback::class.java)
val intAdvCbCaptor2 = ArgumentCaptor.forClass(MdnsInterfaceAdvertiser.Callback::class.java)
- verify(mockDeps).makeAdvertiser(eq(mockSocket1), eq(listOf(TEST_LINKADDR)),
- eq(thread.looper), any(), intAdvCbCaptor1.capture(), eq(TEST_HOSTNAME), any(), any()
+ verify(mockDeps).makeAdvertiser(
+ eq(mockSocket1),
+ eq(listOf(TEST_LINKADDR)),
+ eq(thread.looper),
+ any(),
+ intAdvCbCaptor1.capture(),
+ eq(TEST_HOSTNAME),
+ any(),
+ any()
)
- verify(mockDeps).makeAdvertiser(eq(mockSocket2), eq(listOf(TEST_LINKADDR)),
- eq(thread.looper), any(), intAdvCbCaptor2.capture(), eq(TEST_HOSTNAME), any(), any()
+ verify(mockDeps).makeAdvertiser(
+ eq(mockSocket2),
+ eq(listOf(TEST_LINKADDR)),
+ eq(thread.looper),
+ any(),
+ intAdvCbCaptor2.capture(),
+ eq(TEST_HOSTNAME),
+ any(),
+ any()
)
verify(mockInterfaceAdvertiser1).addService(
- anyInt(), eq(ALL_NETWORKS_SERVICE_SUBTYPE), any())
+ anyInt(),
+ eq(ALL_NETWORKS_SERVICE_SUBTYPE),
+ any()
+ )
verify(mockInterfaceAdvertiser2).addService(
- anyInt(), eq(ALL_NETWORKS_SERVICE_SUBTYPE), any())
+ anyInt(),
+ eq(ALL_NETWORKS_SERVICE_SUBTYPE),
+ any()
+ )
doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
postSync { intAdvCbCaptor1.value.onServiceProbingSucceeded(
- mockInterfaceAdvertiser1, SERVICE_ID_1) }
+ mockInterfaceAdvertiser1,
+ SERVICE_ID_1
+ ) }
verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICEINFO))
// Need both advertisers to finish probing and call onRegisterServiceSucceeded
verify(cb, never()).onRegisterServiceSucceeded(anyInt(), any())
doReturn(false).`when`(mockInterfaceAdvertiser2).isProbing(SERVICE_ID_1)
postSync { intAdvCbCaptor2.value.onServiceProbingSucceeded(
- mockInterfaceAdvertiser2, SERVICE_ID_1) }
+ mockInterfaceAdvertiser2,
+ SERVICE_ID_1
+ ) }
verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE2), eq(OFFLOAD_SERVICEINFO))
- verify(cb).onRegisterServiceSucceeded(eq(SERVICE_ID_1),
- argThat { it.matches(ALL_NETWORKS_SERVICE_SUBTYPE) })
+ verify(cb).onRegisterServiceSucceeded(
+ eq(SERVICE_ID_1),
+ argThat { it.matches(ALL_NETWORKS_SERVICE_SUBTYPE) }
+ )
// Services are conflicted.
postSync {
@@ -378,19 +465,30 @@
val advertiser =
MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
postSync {
- advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1, DEFAULT_ADVERTISING_OPTION,
- TEST_CLIENT_UID_1)
- advertiser.addOrUpdateService(SERVICE_ID_2,
+ advertiser.addOrUpdateService(
+ SERVICE_ID_1,
+ SERVICE_1,
+ DEFAULT_ADVERTISING_OPTION,
+ TEST_CLIENT_UID_1
+ )
+ advertiser.addOrUpdateService(
+ SERVICE_ID_2,
NsdServiceInfo("TestService2", "_PRIORITYTEST._udp").apply {
port = 12345
hostAddresses = listOf(TEST_ADDR)
- }, DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1)
+ },
+ DEFAULT_ADVERTISING_OPTION,
+ TEST_CLIENT_UID_1
+ )
advertiser.addOrUpdateService(
SERVICE_ID_3,
NsdServiceInfo("TestService3", "_notprioritized._tcp").apply {
port = 12345
hostAddresses = listOf(TEST_ADDR)
- }, DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1)
+ },
+ DEFAULT_ADVERTISING_OPTION,
+ TEST_CLIENT_UID_1
+ )
}
val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
@@ -400,8 +498,15 @@
postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
val intAdvCbCaptor1 = ArgumentCaptor.forClass(MdnsInterfaceAdvertiser.Callback::class.java)
- verify(mockDeps).makeAdvertiser(eq(mockSocket1), eq(listOf(TEST_LINKADDR)),
- eq(thread.looper), any(), intAdvCbCaptor1.capture(), eq(TEST_HOSTNAME), any(), any()
+ verify(mockDeps).makeAdvertiser(
+ eq(mockSocket1),
+ eq(listOf(TEST_LINKADDR)),
+ eq(thread.looper),
+ any(),
+ intAdvCbCaptor1.capture(),
+ eq(TEST_HOSTNAME),
+ any(),
+ any()
)
doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
@@ -433,30 +538,126 @@
}
@Test
+ fun testAddService_NoOffloadForServiceTypeInDenyList() {
+ val advertiser =
+ MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
+ postSync {
+ advertiser.addOrUpdateService(
+ SERVICE_ID_1,
+ GOOGLEZONE_SERVICE,
+ DEFAULT_ADVERTISING_OPTION,
+ TEST_CLIENT_UID_1
+ )
+ }
+ val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
+ verify(socketProvider).requestSocket(eq(SERVICE_1.network), socketCbCaptor.capture())
+
+ val socketCb = socketCbCaptor.value
+ postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
+
+ val intAdvCbCaptor1 = ArgumentCaptor.forClass(MdnsInterfaceAdvertiser.Callback::class.java)
+ verify(mockDeps).makeAdvertiser(
+ eq(mockSocket1),
+ eq(listOf(TEST_LINKADDR)),
+ eq(thread.looper),
+ any(),
+ intAdvCbCaptor1.capture(),
+ eq(TEST_HOSTNAME),
+ any(),
+ any()
+ )
+
+ doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
+ postSync {
+ intAdvCbCaptor1.value.onServiceProbingSucceeded(mockInterfaceAdvertiser1, SERVICE_ID_1)
+ }
+
+ verify(cb, never()).onOffloadStartOrUpdate(eq(TEST_INTERFACE1), any())
+ }
+
+ @Test
+ fun testAddService_NoSubtypeForGoogleCastOffload() {
+ val advertiser =
+ MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
+ postSync {
+ advertiser.addOrUpdateService(
+ SERVICE_ID_1,
+ GOOGLECAST_SERVICE,
+ DEFAULT_ADVERTISING_OPTION,
+ TEST_CLIENT_UID_1
+ )
+ }
+ val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
+ verify(socketProvider).requestSocket(eq(SERVICE_1.network), socketCbCaptor.capture())
+
+ val socketCb = socketCbCaptor.value
+ postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
+
+ val intAdvCbCaptor1 = ArgumentCaptor.forClass(MdnsInterfaceAdvertiser.Callback::class.java)
+ verify(mockDeps).makeAdvertiser(
+ eq(mockSocket1),
+ eq(listOf(TEST_LINKADDR)),
+ eq(thread.looper),
+ any(),
+ intAdvCbCaptor1.capture(),
+ eq(TEST_HOSTNAME),
+ any(),
+ any()
+ )
+
+ doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
+ postSync {
+ intAdvCbCaptor1.value.onServiceProbingSucceeded(mockInterfaceAdvertiser1, SERVICE_ID_1)
+ }
+
+ verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICE_INFO_GOOGLECAST))
+ }
+
+ @Test
fun testAddService_Conflicts() {
val advertiser =
MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
- postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
- DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
+ postSync { advertiser.addOrUpdateService(
+ SERVICE_ID_1,
+ SERVICE_1,
+ DEFAULT_ADVERTISING_OPTION,
+ TEST_CLIENT_UID_1
+ ) }
val oneNetSocketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(TEST_NETWORK_1), oneNetSocketCbCaptor.capture())
val oneNetSocketCb = oneNetSocketCbCaptor.value
// Register a service with the same name on all networks (name conflict)
- postSync { advertiser.addOrUpdateService(SERVICE_ID_2, ALL_NETWORKS_SERVICE,
- DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
+ postSync { advertiser.addOrUpdateService(
+ SERVICE_ID_2,
+ ALL_NETWORKS_SERVICE,
+ DEFAULT_ADVERTISING_OPTION,
+ TEST_CLIENT_UID_1
+ ) }
val allNetSocketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(null), allNetSocketCbCaptor.capture())
val allNetSocketCb = allNetSocketCbCaptor.value
- postSync { advertiser.addOrUpdateService(LONG_SERVICE_ID_1, LONG_SERVICE_1,
- DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
- postSync { advertiser.addOrUpdateService(LONG_SERVICE_ID_2, LONG_ALL_NETWORKS_SERVICE,
- DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
+ postSync { advertiser.addOrUpdateService(
+ LONG_SERVICE_ID_1,
+ LONG_SERVICE_1,
+ DEFAULT_ADVERTISING_OPTION,
+ TEST_CLIENT_UID_1
+ ) }
+ postSync { advertiser.addOrUpdateService(
+ LONG_SERVICE_ID_2,
+ LONG_ALL_NETWORKS_SERVICE,
+ DEFAULT_ADVERTISING_OPTION,
+ TEST_CLIENT_UID_1
+ ) }
- postSync { advertiser.addOrUpdateService(CASE_INSENSITIVE_TEST_SERVICE_ID,
- ALL_NETWORKS_SERVICE_2, DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
+ postSync { advertiser.addOrUpdateService(
+ CASE_INSENSITIVE_TEST_SERVICE_ID,
+ ALL_NETWORKS_SERVICE_2,
+ DEFAULT_ADVERTISING_OPTION,
+ TEST_CLIENT_UID_1
+ ) }
// Callbacks for matching network and all networks both get the socket
postSync {
@@ -465,7 +666,9 @@
}
val expectedRenamed = NsdServiceInfo(
- "${ALL_NETWORKS_SERVICE.serviceName} (2)", ALL_NETWORKS_SERVICE.serviceType).apply {
+ "${ALL_NETWORKS_SERVICE.serviceName} (2)",
+ ALL_NETWORKS_SERVICE.serviceType
+ ).apply {
port = ALL_NETWORKS_SERVICE.port
hostAddresses = ALL_NETWORKS_SERVICE.hostAddresses
network = ALL_NETWORKS_SERVICE.network
@@ -473,14 +676,16 @@
val expectedLongRenamed = NsdServiceInfo(
"${LONG_ALL_NETWORKS_SERVICE.serviceName.dropLast(4)} (2)",
- LONG_ALL_NETWORKS_SERVICE.serviceType).apply {
+ LONG_ALL_NETWORKS_SERVICE.serviceType
+ ).apply {
port = LONG_ALL_NETWORKS_SERVICE.port
hostAddresses = LONG_ALL_NETWORKS_SERVICE.hostAddresses
network = LONG_ALL_NETWORKS_SERVICE.network
}
val expectedCaseInsensitiveRenamed = NsdServiceInfo(
- "${ALL_NETWORKS_SERVICE_2.serviceName} (3)", ALL_NETWORKS_SERVICE_2.serviceType
+ "${ALL_NETWORKS_SERVICE_2.serviceName} (3)",
+ ALL_NETWORKS_SERVICE_2.serviceType
).apply {
port = ALL_NETWORKS_SERVICE_2.port
hostAddresses = ALL_NETWORKS_SERVICE_2.hostAddresses
@@ -488,30 +693,58 @@
}
val intAdvCbCaptor = ArgumentCaptor.forClass(MdnsInterfaceAdvertiser.Callback::class.java)
- verify(mockDeps).makeAdvertiser(eq(mockSocket1), eq(listOf(TEST_LINKADDR)),
- eq(thread.looper), any(), intAdvCbCaptor.capture(), eq(TEST_HOSTNAME), any(), any()
+ verify(mockDeps).makeAdvertiser(
+ eq(mockSocket1),
+ eq(listOf(TEST_LINKADDR)),
+ eq(thread.looper),
+ any(),
+ intAdvCbCaptor.capture(),
+ eq(TEST_HOSTNAME),
+ any(),
+ any()
)
- verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1),
- argThat { it.matches(SERVICE_1) }, any())
- verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_2),
- argThat { it.matches(expectedRenamed) }, any())
- verify(mockInterfaceAdvertiser1).addService(eq(LONG_SERVICE_ID_1),
- argThat { it.matches(LONG_SERVICE_1) }, any())
- verify(mockInterfaceAdvertiser1).addService(eq(LONG_SERVICE_ID_2),
- argThat { it.matches(expectedLongRenamed) }, any())
- verify(mockInterfaceAdvertiser1).addService(eq(CASE_INSENSITIVE_TEST_SERVICE_ID),
- argThat { it.matches(expectedCaseInsensitiveRenamed) }, any())
+ verify(mockInterfaceAdvertiser1).addService(
+ eq(SERVICE_ID_1),
+ argThat { it.matches(SERVICE_1) },
+ any()
+ )
+ verify(mockInterfaceAdvertiser1).addService(
+ eq(SERVICE_ID_2),
+ argThat { it.matches(expectedRenamed) },
+ any()
+ )
+ verify(mockInterfaceAdvertiser1).addService(
+ eq(LONG_SERVICE_ID_1),
+ argThat { it.matches(LONG_SERVICE_1) },
+ any()
+ )
+ verify(mockInterfaceAdvertiser1).addService(
+ eq(LONG_SERVICE_ID_2),
+ argThat { it.matches(expectedLongRenamed) },
+ any()
+ )
+ verify(mockInterfaceAdvertiser1).addService(
+ eq(CASE_INSENSITIVE_TEST_SERVICE_ID),
+ argThat { it.matches(expectedCaseInsensitiveRenamed) },
+ any()
+ )
doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
postSync { intAdvCbCaptor.value.onServiceProbingSucceeded(
- mockInterfaceAdvertiser1, SERVICE_ID_1) }
+ mockInterfaceAdvertiser1,
+ SERVICE_ID_1
+ ) }
verify(cb).onRegisterServiceSucceeded(eq(SERVICE_ID_1), argThat { it.matches(SERVICE_1) })
doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_2)
postSync { intAdvCbCaptor.value.onServiceProbingSucceeded(
- mockInterfaceAdvertiser1, SERVICE_ID_2) }
- verify(cb).onRegisterServiceSucceeded(eq(SERVICE_ID_2),
- argThat { it.matches(expectedRenamed) })
+ mockInterfaceAdvertiser1,
+ SERVICE_ID_2
+ ) }
+ verify(cb).onRegisterServiceSucceeded(
+ eq(SERVICE_ID_2),
+ argThat { it.matches(expectedRenamed) }
+ )
postSync { oneNetSocketCb.onInterfaceDestroyed(TEST_SOCKETKEY_1, mockSocket1) }
postSync { allNetSocketCb.onInterfaceDestroyed(TEST_SOCKETKEY_1, mockSocket1) }
@@ -523,10 +756,21 @@
@Test
fun testAddOrUpdateService_Updates() {
val advertiser =
- MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags,
- context)
- postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE,
- DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
+ MdnsAdvertiser(
+ thread.looper,
+ socketProvider,
+ cb,
+ mockDeps,
+ sharedlog,
+ flags,
+ context
+ )
+ postSync { advertiser.addOrUpdateService(
+ SERVICE_ID_1,
+ ALL_NETWORKS_SERVICE,
+ DEFAULT_ADVERTISING_OPTION,
+ TEST_CLIENT_UID_1
+ ) }
val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(null), socketCbCaptor.capture())
@@ -534,41 +778,70 @@
val socketCb = socketCbCaptor.value
postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
- verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1),
- argThat { it.matches(ALL_NETWORKS_SERVICE) }, any())
+ verify(mockInterfaceAdvertiser1).addService(
+ eq(SERVICE_ID_1),
+ argThat { it.matches(ALL_NETWORKS_SERVICE) },
+ any()
+ )
val updateOptions = MdnsAdvertisingOptions.newBuilder().setIsOnlyUpdate(true).build()
// Update with serviceId that is not registered yet should fail
- postSync { advertiser.addOrUpdateService(SERVICE_ID_2, ALL_NETWORKS_SERVICE_SUBTYPE,
- updateOptions, TEST_CLIENT_UID_1) }
+ postSync { advertiser.addOrUpdateService(
+ SERVICE_ID_2,
+ ALL_NETWORKS_SERVICE_SUBTYPE,
+ updateOptions,
+ TEST_CLIENT_UID_1
+ ) }
verify(cb).onRegisterServiceFailed(SERVICE_ID_2, NsdManager.FAILURE_INTERNAL_ERROR)
// Update service with different NsdServiceInfo should fail
- postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1_SUBTYPE, updateOptions,
- TEST_CLIENT_UID_1) }
+ postSync { advertiser.addOrUpdateService(
+ SERVICE_ID_1,
+ SERVICE_1_SUBTYPE,
+ updateOptions,
+ TEST_CLIENT_UID_1
+ ) }
verify(cb).onRegisterServiceFailed(SERVICE_ID_1, NsdManager.FAILURE_INTERNAL_ERROR)
// Update service with same NsdServiceInfo but different subType should succeed
- postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE_SUBTYPE,
- updateOptions, TEST_CLIENT_UID_1) }
+ postSync { advertiser.addOrUpdateService(
+ SERVICE_ID_1,
+ ALL_NETWORKS_SERVICE_SUBTYPE,
+ updateOptions,
+ TEST_CLIENT_UID_1
+ ) }
verify(mockInterfaceAdvertiser1).updateService(eq(SERVICE_ID_1), eq(setOf(TEST_SUBTYPE)))
// Newly created MdnsInterfaceAdvertiser will get addService() call.
postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_2, mockSocket2, listOf(TEST_LINKADDR2)) }
- verify(mockInterfaceAdvertiser2).addService(eq(SERVICE_ID_1),
- argThat { it.matches(ALL_NETWORKS_SERVICE_SUBTYPE) }, any())
+ verify(mockInterfaceAdvertiser2).addService(
+ eq(SERVICE_ID_1),
+ argThat { it.matches(ALL_NETWORKS_SERVICE_SUBTYPE) },
+ any()
+ )
}
@Test
fun testAddOrUpdateService_customTtl_registeredSuccess() {
val advertiser = MdnsAdvertiser(
- thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
+ thread.looper,
+ socketProvider,
+ cb,
+ mockDeps,
+ sharedlog,
+ flags,
+ context
+ )
val updateOptions =
MdnsAdvertisingOptions.newBuilder().setTtl(Duration.ofSeconds(30)).build()
- postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE,
- updateOptions, TEST_CLIENT_UID_1) }
+ postSync { advertiser.addOrUpdateService(
+ SERVICE_ID_1,
+ ALL_NETWORKS_SERVICE,
+ updateOptions,
+ TEST_CLIENT_UID_1
+ ) }
val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(null), socketCbCaptor.capture())
@@ -582,8 +855,12 @@
val advertiser =
MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
verify(mockDeps, times(1)).generateHostname(anyBoolean())
- postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
- DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
+ postSync { advertiser.addOrUpdateService(
+ SERVICE_ID_1,
+ SERVICE_1,
+ DEFAULT_ADVERTISING_OPTION,
+ TEST_CLIENT_UID_1
+ ) }
postSync { advertiser.removeService(SERVICE_ID_1) }
verify(mockDeps, times(2)).generateHostname(anyBoolean())
}
@@ -593,8 +870,12 @@
val flags = MdnsFeatureFlags.newBuilder().setIsShortHostnamesEnabled(shortHostname).build()
val advertiser =
MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
- postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
- DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
+ postSync { advertiser.addOrUpdateService(
+ SERVICE_ID_1,
+ SERVICE_1,
+ DEFAULT_ADVERTISING_OPTION,
+ TEST_CLIENT_UID_1
+ ) }
val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(TEST_NETWORK_1), socketCbCaptor.capture())
@@ -621,8 +902,10 @@
val hostname = doHostnameGenerationTest(shortHostname = true)
// Short hostnames are [8 uppercase letters or digits].local
assertEquals(2, hostname.size)
- assertTrue(Regex("Android_[A-Z0-9]{8}").matches(hostname[0]),
- "Unexpected hostname: ${hostname.contentToString()}")
+ assertTrue(
+ Regex("Android_[A-Z0-9]{8}").matches(hostname[0]),
+ "Unexpected hostname: ${hostname.contentToString()}"
+ )
assertEquals("local", hostname[1])
}
@@ -631,8 +914,10 @@
val hostname = doHostnameGenerationTest(shortHostname = false)
// Long hostnames are Android_[32 lowercase hex characters].local
assertEquals(2, hostname.size)
- assertTrue(Regex("Android_[a-f0-9]{32}").matches(hostname[0]),
- "Unexpected hostname: ${hostname.contentToString()}")
+ assertTrue(
+ Regex("Android_[a-f0-9]{32}").matches(hostname[0]),
+ "Unexpected hostname: ${hostname.contentToString()}"
+ )
assertEquals("local", hostname[1])
}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
index ab2fb99..758b822 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
@@ -19,8 +19,6 @@
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
@@ -35,12 +33,10 @@
import android.net.Network;
import android.os.Handler;
import android.os.HandlerThread;
-import android.testing.TestableLooper;
import android.text.TextUtils;
import android.util.Pair;
import com.android.net.module.util.SharedLog;
-import com.android.server.connectivity.mdns.MdnsDiscoveryManager.DiscoveryExecutor;
import com.android.server.connectivity.mdns.MdnsSocketClientBase.SocketCreationCallback;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
@@ -60,9 +56,7 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
-import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
/** Tests for {@link MdnsDiscoveryManager}. */
@DevSdkIgnoreRunner.MonitorThreadLeak
@@ -435,48 +429,6 @@
}
@Test
- public void testDiscoveryExecutor() throws Exception {
- final TestableLooper testableLooper = new TestableLooper(thread.getLooper());
- final DiscoveryExecutor executor = new DiscoveryExecutor(testableLooper.getLooper());
- try {
- // Verify the checkAndRunOnHandlerThread method
- final CompletableFuture<Boolean> future1 = new CompletableFuture<>();
- executor.checkAndRunOnHandlerThread(()-> future1.complete(true));
- assertTrue(future1.isDone());
- assertTrue(future1.get(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS));
-
- // Verify the execute method
- final CompletableFuture<Boolean> future2 = new CompletableFuture<>();
- executor.execute(()-> future2.complete(true));
- testableLooper.processAllMessages();
- assertTrue(future2.isDone());
- assertTrue(future2.get(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS));
-
- // Verify the executeDelayed method
- final CompletableFuture<Boolean> future3 = new CompletableFuture<>();
- // Schedule a task with 999 ms delay
- executor.executeDelayed(()-> future3.complete(true), 999L);
- testableLooper.processAllMessages();
- assertFalse(future3.isDone());
-
- // 500 ms have elapsed but do not exceed the target time (999 ms)
- // The function should not be executed.
- testableLooper.moveTimeForward(500L);
- testableLooper.processAllMessages();
- assertFalse(future3.isDone());
-
- // 500 ms have elapsed again and have exceeded the target time (999 ms).
- // The function should be executed.
- testableLooper.moveTimeForward(500L);
- testableLooper.processAllMessages();
- assertTrue(future3.isDone());
- assertTrue(future3.get(500L, TimeUnit.MILLISECONDS));
- } finally {
- testableLooper.destroy();
- }
- }
-
- @Test
public void testRemoveServicesAfterAllListenersUnregistered() throws IOException {
final MdnsFeatureFlags mdnsFeatureFlags = MdnsFeatureFlags.newBuilder()
.setIsCachedServicesRemovalEnabled(true)
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
index 0a8f108..976dfa9 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
@@ -208,7 +208,10 @@
@Test
fun testServiceExpiredAndSendCallbacks() {
val serviceCache = MdnsServiceCache(
- thread.looper, makeFlags(isExpiredServicesRemovalEnabled = true), clock)
+ thread.looper,
+ makeFlags(isExpiredServicesRemovalEnabled = true),
+ clock
+ )
// Register service expired callbacks
val callback1 = ExpiredRecord()
val callback2 = ExpiredRecord()
@@ -218,12 +221,21 @@
doReturn(TEST_ELAPSED_REALTIME_MS).`when`(clock).elapsedRealtime()
// Add multiple services with different ttl time.
- addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_1, SERVICE_TYPE_1,
- DEFAULT_TTL_TIME_MS))
- addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_2, SERVICE_TYPE_1,
- DEFAULT_TTL_TIME_MS + 20L))
- addOrUpdateService(serviceCache, cacheKey2, createResponse(SERVICE_NAME_3, SERVICE_TYPE_2,
- DEFAULT_TTL_TIME_MS + 10L))
+ addOrUpdateService(serviceCache, cacheKey1, createResponse(
+ SERVICE_NAME_1,
+ SERVICE_TYPE_1,
+ DEFAULT_TTL_TIME_MS
+ ))
+ addOrUpdateService(serviceCache, cacheKey1, createResponse(
+ SERVICE_NAME_2,
+ SERVICE_TYPE_1,
+ DEFAULT_TTL_TIME_MS + 20L
+ ))
+ addOrUpdateService(serviceCache, cacheKey2, createResponse(
+ SERVICE_NAME_3,
+ SERVICE_TYPE_2,
+ DEFAULT_TTL_TIME_MS + 10L
+ ))
// Check the service expiration immediately. Should be no callback.
assertEquals(2, getServices(serviceCache, cacheKey1).size)
@@ -252,16 +264,25 @@
@Test
fun testRemoveExpiredServiceWhenGetting() {
val serviceCache = MdnsServiceCache(
- thread.looper, makeFlags(isExpiredServicesRemovalEnabled = true), clock)
+ thread.looper,
+ makeFlags(isExpiredServicesRemovalEnabled = true),
+ clock
+ )
doReturn(TEST_ELAPSED_REALTIME_MS).`when`(clock).elapsedRealtime()
- addOrUpdateService(serviceCache, cacheKey1,
- createResponse(SERVICE_NAME_1, SERVICE_TYPE_1, 1L /* ttlTime */))
+ addOrUpdateService(
+ serviceCache,
+ cacheKey1,
+ createResponse(SERVICE_NAME_1, SERVICE_TYPE_1, 1L /* ttlTime */)
+ )
doReturn(TEST_ELAPSED_REALTIME_MS + 2L).`when`(clock).elapsedRealtime()
assertNull(getService(serviceCache, SERVICE_NAME_1, cacheKey1))
- addOrUpdateService(serviceCache, cacheKey2,
- createResponse(SERVICE_NAME_2, SERVICE_TYPE_2, 3L /* ttlTime */))
+ addOrUpdateService(
+ serviceCache,
+ cacheKey2,
+ createResponse(SERVICE_NAME_2, SERVICE_TYPE_2, 3L /* ttlTime */)
+ )
doReturn(TEST_ELAPSED_REALTIME_MS + 4L).`when`(clock).elapsedRealtime()
assertEquals(0, getServices(serviceCache, cacheKey2).size)
}
@@ -334,8 +355,11 @@
): MdnsResponse {
val serviceName = "$serviceInstanceName.$serviceType".split(".").toTypedArray()
val response = MdnsResponse(
- 0 /* now */, "$serviceInstanceName.$serviceType".split(".").toTypedArray(),
- socketKey.interfaceIndex, socketKey.network)
+ 0 /* now */,
+ "$serviceInstanceName.$serviceType".split(".").toTypedArray(),
+ socketKey.interfaceIndex,
+ socketKey.network
+ )
// Set PTR record
val pointerRecord = MdnsPointerRecord(
@@ -343,7 +367,8 @@
TEST_ELAPSED_REALTIME_MS /* receiptTimeMillis */,
false /* cacheFlush */,
ttlTime /* ttlMillis */,
- serviceName)
+ serviceName
+ )
response.addPointerRecord(pointerRecord)
// Set SRV record.
@@ -355,7 +380,8 @@
0 /* servicePriority */,
0 /* serviceWeight */,
12345 /* port */,
- arrayOf("hostname"))
+ arrayOf("hostname")
+ )
response.serviceRecord = serviceRecord
return response
}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
index 67f9d9c..dad03e0 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -59,6 +59,7 @@
import android.text.TextUtils;
import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.RealtimeScheduler;
import com.android.net.module.util.SharedLog;
import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry;
import com.android.server.connectivity.mdns.util.MdnsUtils;
@@ -127,6 +128,8 @@
private SharedLog mockSharedLog;
@Mock
private MdnsServiceTypeClient.Dependencies mockDeps;
+ @Mock
+ private RealtimeScheduler mockRealtimeScheduler;
@Captor
private ArgumentCaptor<MdnsServiceInfo> serviceInfoCaptor;
@@ -145,6 +148,7 @@
private Message delayMessage = null;
private Handler realHandler = null;
private MdnsFeatureFlags featureFlags = MdnsFeatureFlags.newBuilder().build();
+ private Message message = null;
@Before
@SuppressWarnings("DoNotMock")
@@ -244,10 +248,21 @@
return true;
}).when(mockDeps).sendMessage(any(Handler.class), any(Message.class));
- client = makeMdnsServiceTypeClient();
+ doAnswer(inv -> {
+ realHandler = (Handler) inv.getArguments()[0];
+ return mockRealtimeScheduler;
+ }).when(mockDeps).createRealtimeScheduler(any(Handler.class));
+
+ doAnswer(inv -> {
+ message = (Message) inv.getArguments()[0];
+ latestDelayMs = (long) inv.getArguments()[1];
+ return null;
+ }).when(mockRealtimeScheduler).sendDelayedMessage(any(), anyLong());
+
+ client = makeMdnsServiceTypeClient(featureFlags);
}
- private MdnsServiceTypeClient makeMdnsServiceTypeClient() {
+ private MdnsServiceTypeClient makeMdnsServiceTypeClient(MdnsFeatureFlags featureFlags) {
return new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
serviceCache, featureFlags);
@@ -1926,9 +1941,7 @@
@Test
public void testSendQueryWithKnownAnswers() throws Exception {
- client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
- mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
- serviceCache,
+ client = makeMdnsServiceTypeClient(
MdnsFeatureFlags.newBuilder().setIsQueryWithKnownAnswerEnabled(true).build());
doCallRealMethod().when(mockDeps).getDatagramPacketsFromMdnsPacket(
@@ -1990,9 +2003,7 @@
@Test
public void testSendQueryWithSubTypeWithKnownAnswers() throws Exception {
- client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
- mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
- serviceCache,
+ client = makeMdnsServiceTypeClient(
MdnsFeatureFlags.newBuilder().setIsQueryWithKnownAnswerEnabled(true).build());
doCallRealMethod().when(mockDeps).getDatagramPacketsFromMdnsPacket(
@@ -2114,6 +2125,73 @@
assertEquals(9680L, latestDelayMs);
}
+ @Test
+ public void sendQueries_AccurateDelayCallback() {
+ client = makeMdnsServiceTypeClient(
+ MdnsFeatureFlags.newBuilder().setIsAccurateDelayCallbackEnabled(true).build());
+
+ final int numOfQueriesBeforeBackoff = 2;
+ final MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+ .addSubtype(SUBTYPE)
+ .setQueryMode(AGGRESSIVE_QUERY_MODE)
+ .setNumOfQueriesBeforeBackoff(numOfQueriesBeforeBackoff)
+ .build();
+ startSendAndReceive(mockListenerOne, searchOptions);
+ verify(mockRealtimeScheduler, times(1)).removeDelayedMessage(EVENT_START_QUERYTASK);
+
+ // Verify that the first query has been sent.
+ verifyAndSendQuery(0 /* index */, 0 /* timeInMs */, true /* expectsUnicastResponse */,
+ true /* multipleSocketDiscovery */, 1 /* scheduledCount */,
+ 1 /* sendMessageCount */, true /* useAccurateDelayCallback */);
+
+ // Verify that the second query has been sent
+ verifyAndSendQuery(1 /* index */, 0 /* timeInMs */, false /* expectsUnicastResponse */,
+ true /* multipleSocketDiscovery */, 2 /* scheduledCount */,
+ 2 /* sendMessageCount */, true /* useAccurateDelayCallback */);
+
+ // Verify that the third query has been sent
+ verifyAndSendQuery(2 /* index */, TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS,
+ false /* expectsUnicastResponse */, true /* multipleSocketDiscovery */,
+ 3 /* scheduledCount */, 3 /* sendMessageCount */,
+ true /* useAccurateDelayCallback */);
+
+ // In backoff mode, the current scheduled task will be canceled and reschedule if the
+ // 0.8 * smallestRemainingTtl is larger than time to next run.
+ long currentTime = TEST_TTL / 2 + TEST_ELAPSED_REALTIME;
+ doReturn(currentTime).when(mockDecoderClock).elapsedRealtime();
+ doReturn(true).when(mockRealtimeScheduler).hasDelayedMessage(EVENT_START_QUERYTASK);
+ processResponse(createResponse(
+ "service-instance-1", "192.0.2.123", 5353,
+ SERVICE_TYPE_LABELS,
+ Collections.emptyMap(), TEST_TTL), socketKey);
+ // Verify that the message removal occurred.
+ verify(mockRealtimeScheduler, times(6)).removeDelayedMessage(EVENT_START_QUERYTASK);
+ assertNotNull(message);
+ verifyAndSendQuery(3 /* index */, (long) (TEST_TTL / 2 * 0.8) /* timeInMs */,
+ true /* expectsUnicastResponse */, true /* multipleSocketDiscovery */,
+ 5 /* scheduledCount */, 4 /* sendMessageCount */,
+ true /* useAccurateDelayCallback */);
+
+ // Stop sending packets.
+ stopSendAndReceive(mockListenerOne);
+ verify(mockRealtimeScheduler, times(8)).removeDelayedMessage(EVENT_START_QUERYTASK);
+ }
+
+ @Test
+ public void testTimerFdCloseProperly() {
+ client = makeMdnsServiceTypeClient(
+ MdnsFeatureFlags.newBuilder().setIsAccurateDelayCallbackEnabled(true).build());
+
+ // Start query
+ startSendAndReceive(mockListenerOne, MdnsSearchOptions.newBuilder().build());
+ verify(mockRealtimeScheduler, times(1)).removeDelayedMessage(EVENT_START_QUERYTASK);
+
+ // Stop query and verify the close() method has been called.
+ stopSendAndReceive(mockListenerOne);
+ verify(mockRealtimeScheduler, times(2)).removeDelayedMessage(EVENT_START_QUERYTASK);
+ verify(mockRealtimeScheduler).close();
+ }
+
private static MdnsServiceInfo matchServiceName(String name) {
return argThat(info -> info.getServiceInstanceName().equals(name));
}
@@ -2127,9 +2205,22 @@
private void verifyAndSendQuery(int index, long timeInMs, boolean expectsUnicastResponse,
boolean multipleSocketDiscovery, int scheduledCount) {
- // Dispatch the message
- if (delayMessage != null && realHandler != null) {
- dispatchMessage();
+ verifyAndSendQuery(index, timeInMs, expectsUnicastResponse,
+ multipleSocketDiscovery, scheduledCount, index + 1 /* sendMessageCount */,
+ false /* useAccurateDelayCallback */);
+ }
+
+ private void verifyAndSendQuery(int index, long timeInMs, boolean expectsUnicastResponse,
+ boolean multipleSocketDiscovery, int scheduledCount, int sendMessageCount,
+ boolean useAccurateDelayCallback) {
+ if (useAccurateDelayCallback && message != null && realHandler != null) {
+ runOnHandler(() -> realHandler.dispatchMessage(message));
+ message = null;
+ } else {
+ // Dispatch the message
+ if (delayMessage != null && realHandler != null) {
+ dispatchMessage();
+ }
}
assertEquals(timeInMs, latestDelayMs);
currentThreadExecutor.getAndClearLastScheduledRunnable().run();
@@ -2152,11 +2243,16 @@
eq(socketKey), eq(false));
}
}
- verify(mockDeps, times(index + 1))
+ verify(mockDeps, times(sendMessageCount))
.sendMessage(any(Handler.class), any(Message.class));
// Verify the task has been scheduled.
- verify(mockDeps, times(scheduledCount))
- .sendMessageDelayed(any(Handler.class), any(Message.class), anyLong());
+ if (useAccurateDelayCallback) {
+ verify(mockRealtimeScheduler, times(scheduledCount))
+ .sendDelayedMessage(any(), anyLong());
+ } else {
+ verify(mockDeps, times(scheduledCount))
+ .sendMessageDelayed(any(Handler.class), any(Message.class), anyLong());
+ }
}
private static String[] getTestServiceName(String instanceName) {
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java
index 1cc9985..f763bae 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java
@@ -610,6 +610,7 @@
@Test
public void testSocketCreatedForMulticastInterface() throws Exception {
+ doReturn(true).when(mTestNetworkIfaceWrapper).isPointToPoint();
doReturn(true).when(mTestNetworkIfaceWrapper).supportsMulticast();
startMonitoringSockets();
@@ -621,18 +622,6 @@
}
@Test
- public void testNoSocketCreatedForPTPInterface() throws Exception {
- doReturn(true).when(mTestNetworkIfaceWrapper).isPointToPoint();
- startMonitoringSockets();
-
- final TestSocketCallback testCallback = new TestSocketCallback();
- runOnHandler(() -> mSocketProvider.requestSocket(TEST_NETWORK, testCallback));
-
- postNetworkAvailable(TRANSPORT_BLUETOOTH);
- testCallback.expectedNoCallback();
- }
-
- @Test
public void testNoSocketCreatedForVPNInterface() throws Exception {
// VPN interfaces generally also have IFF_POINTOPOINT, but even if they don't, they should
// not be included even with TRANSPORT_WIFI.
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt
index 8155fd0..06cb7ee 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt
@@ -21,7 +21,10 @@
import android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED
import android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED
import android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED
+import android.net.InetAddresses
+import android.net.LinkProperties
import android.os.Build
+import android.os.Build.VERSION_CODES
import androidx.test.filters.SmallTest
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter
@@ -33,11 +36,32 @@
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.atLeastOnce
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
+internal val LOCAL_DNS = InetAddresses.parseNumericAddress("224.0.1.2")
+internal val NON_LOCAL_DNS = InetAddresses.parseNumericAddress("76.76.75.75")
+
+private const val IFNAME_1 = "wlan1"
+private const val IFNAME_2 = "wlan2"
+private const val PORT_53 = 53
+private const val PROTOCOL_TCP = 6
+private const val PROTOCOL_UDP = 17
+
+private val lpWithNoLocalDns = LinkProperties().apply {
+ addDnsServer(NON_LOCAL_DNS)
+ interfaceName = IFNAME_1
+}
+
+private val lpWithLocalDns = LinkProperties().apply {
+ addDnsServer(LOCAL_DNS)
+ interfaceName = IFNAME_2
+}
+
@DevSdkIgnoreRunner.MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner::class)
@SmallTest
@@ -69,6 +93,81 @@
}
}
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @Test
+ fun testLocalPrefixesUpdatedInBpfMap() {
+ // Connect Wi-Fi network with non-local dns.
+ val wifiAgent = Agent(nc = defaultNc(), lp = lpWithNoLocalDns)
+ wifiAgent.connect()
+
+ // Verify that block rule is added to BpfMap for local prefixes.
+ verify(bpfNetMaps, atLeastOnce()).addLocalNetAccess(any(), eq(IFNAME_1),
+ any(), eq(0), eq(0), eq(false))
+
+ wifiAgent.disconnect()
+ val cellAgent = Agent(nc = defaultNc(), lp = lpWithLocalDns)
+ cellAgent.connect()
+
+ // Verify that block rule is removed from BpfMap for local prefixes.
+ verify(bpfNetMaps, atLeastOnce()).removeLocalNetAccess(any(), eq(IFNAME_1),
+ any(), eq(0), eq(0))
+
+ cellAgent.disconnect()
+ }
+
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @Test
+ fun testLocalDnsNotUpdatedInBpfMap() {
+ // Connect Wi-Fi network with non-local dns.
+ val wifiAgent = Agent(nc = defaultNc(), lp = lpWithNoLocalDns)
+ wifiAgent.connect()
+
+ // Verify that No allow rule is added to BpfMap since there is no local dns.
+ verify(bpfNetMaps, never()).addLocalNetAccess(any(), any(), any(), any(), any(),
+ eq(true))
+
+ wifiAgent.disconnect()
+ val cellAgent = Agent(nc = defaultNc(), lp = lpWithLocalDns)
+ cellAgent.connect()
+
+ // Verify that No allow rule from port 53 is removed on network change
+ // because no dns was added
+ verify(bpfNetMaps, never()).removeLocalNetAccess(eq(192), eq(IFNAME_1),
+ eq(NON_LOCAL_DNS), any(), eq(PORT_53))
+
+ cellAgent.disconnect()
+ }
+
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @Test
+ fun testLocalDnsUpdatedInBpfMap() {
+ // Connect Wi-Fi network with one local Dns.
+ val wifiAgent = Agent(nc = defaultNc(), lp = lpWithLocalDns)
+ wifiAgent.connect()
+
+ // Verify that allow rule is added to BpfMap for local dns at port 53,
+ // for TCP(=6) protocol
+ verify(bpfNetMaps, atLeastOnce()).addLocalNetAccess(eq(192), eq(IFNAME_2),
+ eq(LOCAL_DNS), eq(PROTOCOL_TCP), eq(PORT_53), eq(true))
+ // And for UDP(=17) protocol
+ verify(bpfNetMaps, atLeastOnce()).addLocalNetAccess(eq(192), eq(IFNAME_2),
+ eq(LOCAL_DNS), eq(PROTOCOL_UDP), eq(PORT_53), eq(true))
+
+ wifiAgent.disconnect()
+ val cellAgent = Agent(nc = defaultNc(), lp = lpWithNoLocalDns)
+ cellAgent.connect()
+
+ // Verify that allow rule is removed for local dns on network change,
+ // for TCP(=6) protocol
+ verify(bpfNetMaps, atLeastOnce()).removeLocalNetAccess(eq(192), eq(IFNAME_2),
+ eq(LOCAL_DNS), eq(PROTOCOL_TCP), eq(PORT_53))
+ // And for UDP(=17) protocol
+ verify(bpfNetMaps, atLeastOnce()).removeLocalNetAccess(eq(192), eq(IFNAME_2),
+ eq(LOCAL_DNS), eq(PROTOCOL_UDP), eq(PORT_53))
+
+ cellAgent.disconnect()
+ }
+
private fun mockDataSaverStatus(status: Int) {
doReturn(status).`when`(context.networkPolicyManager).getRestrictBackgroundStatus(anyInt())
// While the production code dispatches the intent on the handler thread,
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..babcba9
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSL2capProviderTest.kt
@@ -0,0 +1,397 @@
+/*
+ * 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.BluetoothDevice
+import android.bluetooth.BluetoothServerSocket
+import android.bluetooth.BluetoothSocket
+import android.net.INetworkMonitor
+import android.net.INetworkMonitorCallbacks
+import android.net.IpPrefix
+import android.net.L2capNetworkSpecifier
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_6LOWPAN
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_NONE
+import android.net.L2capNetworkSpecifier.ROLE_CLIENT
+import android.net.L2capNetworkSpecifier.ROLE_SERVER
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.MacAddress
+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.net.RouteInfo
+import android.os.Build
+import android.os.HandlerThread
+import android.os.ParcelFileDescriptor
+import com.android.server.net.L2capNetwork.L2capIpClient
+import com.android.server.net.L2capPacketForwarder
+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.anyNetwork
+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.ArgumentMatchers.eq
+import org.mockito.ArgumentMatchers.isNull
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.doThrow
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+
+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 networkMonitor = mock<INetworkMonitor>()
+
+ private val btAdapter = mock<BluetoothAdapter>()
+ private val btDevice = mock<BluetoothDevice>()
+ private val btServerSocket = mock<BluetoothServerSocket>()
+ private val btSocket = mock<BluetoothSocket>()
+ private val tunInterface = mock<ParcelFileDescriptor>()
+ private val l2capIpClient = mock<L2capIpClient>()
+ private val packetForwarder = mock<L2capPacketForwarder>()
+ 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()
+ doReturn(btDevice).`when`(btAdapter).getRemoteDevice(eq(REMOTE_MAC))
+ doReturn(btSocket).`when`(btDevice).createInsecureL2capChannel(eq(PSM))
+
+ 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()
+ doReturn(tunInterface).`when`(providerDeps).createTunInterface(any())
+ doReturn(packetForwarder).`when`(providerDeps)
+ .createL2capPacketForwarder(any(), any(), any(), any(), any())
+ doReturn(l2capIpClient).`when`(providerDeps).createL2capIpClient(any(), any(), any())
+
+ val lp = LinkProperties()
+ val ifname = "l2cap-tun0"
+ lp.setInterfaceName(ifname)
+ lp.addLinkAddress(LinkAddress("fe80::1/64"))
+ lp.addRoute(RouteInfo(IpPrefix("fe80::/64"), null /* nextHop */, ifname))
+ doReturn(lp).`when`(l2capIpClient).start()
+
+ // Note: In order to properly register a NetworkAgent, a NetworkMonitor must be created for
+ // the agent. CSAgentWrapper already does some of this, but requires adding additional
+ // Dependencies to the production code. Create a mocked NM inside this test instead.
+ doAnswer { i ->
+ val cb = i.arguments[2] as INetworkMonitorCallbacks
+ cb.onNetworkMonitorCreated(networkMonitor)
+ }.`when`(networkStack).makeNetworkMonitor(
+ any() /* network */,
+ isNull() /* name */,
+ any() /* callbacks */
+ )
+
+ 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()
+ }
+
+ @Test
+ fun testServerNetwork() {
+ val specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+ .build()
+ val nr = REQUEST.copyWithSpecifier(specifier)
+ val cb = reserveNetwork(nr)
+ cb.expect<Reserved>()
+
+ // Unblock BluetoothServerSocket#accept()
+ doReturn(true).`when`(btSocket).isConnected()
+ acceptQueue.put(Optional.of(btSocket))
+
+ cb.expectAvailableCallbacks(anyNetwork(), validated = false)
+ cb.assertNoCallback()
+ // Verify that packet forwarding was started.
+ // TODO: stop mocking L2capPacketForwarder.
+ verify(providerDeps).createL2capPacketForwarder(any(), any(), any(), any(), any())
+ }
+
+ @Test
+ fun testBluetoothException_createInsecureL2capChannelThrows() {
+ doThrow(IOException()).`when`(btDevice).createInsecureL2capChannel(any())
+
+ val specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_CLIENT)
+ .setHeaderCompression(HEADER_COMPRESSION_NONE)
+ .setRemoteAddress(MacAddress.fromBytes(REMOTE_MAC))
+ .setPsm(PSM)
+ .build()
+ val nr = REQUEST.copyWithSpecifier(specifier)
+ val cb = requestNetwork(nr)
+
+ cb.expect<Unavailable>()
+ }
+
+ @Test
+ fun testBluetoothException_bluetoothSocketConnectThrows() {
+ doThrow(IOException()).`when`(btSocket).connect()
+
+ val specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_CLIENT)
+ .setHeaderCompression(HEADER_COMPRESSION_NONE)
+ .setRemoteAddress(MacAddress.fromBytes(REMOTE_MAC))
+ .setPsm(PSM)
+ .build()
+ val nr = REQUEST.copyWithSpecifier(specifier)
+ val cb = requestNetwork(nr)
+
+ cb.expect<Unavailable>()
+ }
+
+ @Test
+ fun testClientNetwork() {
+ val specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_CLIENT)
+ .setHeaderCompression(HEADER_COMPRESSION_NONE)
+ .setRemoteAddress(MacAddress.fromBytes(REMOTE_MAC))
+ .setPsm(PSM)
+ .build()
+ val nr = REQUEST.copyWithSpecifier(specifier)
+ val cb = requestNetwork(nr)
+ cb.expectAvailableCallbacks(anyNetwork(), validated = false)
+ }
+
+ @Test
+ fun testClientNetwork_headerCompressionMismatch() {
+ var specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_CLIENT)
+ .setHeaderCompression(HEADER_COMPRESSION_NONE)
+ .setRemoteAddress(MacAddress.fromBytes(REMOTE_MAC))
+ .setPsm(PSM)
+ .build()
+ var nr = REQUEST.copyWithSpecifier(specifier)
+ val cb = requestNetwork(nr)
+ cb.expectAvailableCallbacks(anyNetwork(), validated = false)
+
+ specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_CLIENT)
+ .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+ .setRemoteAddress(MacAddress.fromBytes(REMOTE_MAC))
+ .setPsm(PSM)
+ .build()
+ nr = REQUEST.copyWithSpecifier(specifier)
+ val cb2 = requestNetwork(nr)
+ cb2.expect<Unavailable>()
+ }
+
+ @Test
+ fun testClientNetwork_multipleRequests() {
+ val specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_CLIENT)
+ .setHeaderCompression(HEADER_COMPRESSION_NONE)
+ .setRemoteAddress(MacAddress.fromBytes(REMOTE_MAC))
+ .setPsm(PSM)
+ .build()
+ val nr = REQUEST.copyWithSpecifier(specifier)
+ val cb = requestNetwork(nr)
+ cb.expectAvailableCallbacks(anyNetwork(), validated = false)
+
+ val cb2 = requestNetwork(nr)
+ cb2.expectAvailableCallbacks(anyNetwork(), validated = false)
+ }
+}
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
new file mode 100644
index 0000000..84c9835
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalNetworkProtectionTest.kt
@@ -0,0 +1,590 @@
+/*
+ * Copyright (C) 2023 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.net.InetAddresses
+import android.net.IpPrefix
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN
+import android.net.NetworkCapabilities.TRANSPORT_VPN
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
+import com.android.testutils.TestableNetworkCallback
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+private const val LONG_TIMEOUT_MS = 5_000
+private const val PREFIX_LENGTH_IPV4 = 32 + 96
+private const val PREFIX_LENGTH_IPV6 = 32
+private const val WIFI_IFNAME = "wlan0"
+private const val WIFI_IFNAME_2 = "wlan1"
+private const val WIFI_IFNAME_3 = "wlan2"
+
+private val wifiNc = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build()
+
+private fun lp(iface: String, vararg linkAddresses: LinkAddress) = LinkProperties().apply {
+ interfaceName = iface
+ for (linkAddress in linkAddresses) {
+ addLinkAddress(linkAddress)
+ }
+}
+
+private fun nr(transport: Int) = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(transport).apply {
+ if (transport != TRANSPORT_VPN) {
+ addCapability(NET_CAPABILITY_NOT_VPN)
+ }
+ }.build()
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+class CSLocalNetworkProtectionTest : CSTest() {
+ private val LOCAL_IPV6_IP_ADDRESS_PREFIX = IpPrefix("fe80::1cf1:35ff:fe8c:db87/64")
+ private val LOCAL_IPV6_LINK_ADDRESS = LinkAddress(
+ LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress(),
+ LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()
+ )
+
+ private val LOCAL_IPV6_IP_ADDRESS_2_PREFIX =
+ IpPrefix("2601:19b:67f:e220:1cf1:35ff:fe8c:db87/64")
+ private val LOCAL_IPV6_LINK_ADDRESS_2 = LinkAddress(
+ LOCAL_IPV6_IP_ADDRESS_2_PREFIX.getAddress(),
+ LOCAL_IPV6_IP_ADDRESS_2_PREFIX.getPrefixLength()
+ )
+
+ private val LOCAL_IPV6_IP_ADDRESS_3_PREFIX =
+ IpPrefix("fe80::/10")
+ private val LOCAL_IPV6_LINK_ADDRESS_3 = LinkAddress(
+ LOCAL_IPV6_IP_ADDRESS_3_PREFIX.getAddress(),
+ LOCAL_IPV6_IP_ADDRESS_3_PREFIX.getPrefixLength()
+ )
+
+ private val LOCAL_IPV4_IP_ADDRESS_PREFIX_1 = IpPrefix("10.0.0.184/24")
+ private val LOCAL_IPV4_LINK_ADDRRESS_1 =
+ LinkAddress(
+ LOCAL_IPV4_IP_ADDRESS_PREFIX_1.getAddress(),
+ LOCAL_IPV4_IP_ADDRESS_PREFIX_1.getPrefixLength()
+ )
+
+ private val LOCAL_IPV4_IP_ADDRESS_PREFIX_2 = IpPrefix("10.0.255.184/24")
+ private val LOCAL_IPV4_LINK_ADDRRESS_2 =
+ LinkAddress(
+ LOCAL_IPV4_IP_ADDRESS_PREFIX_2.getAddress(),
+ LOCAL_IPV4_IP_ADDRESS_PREFIX_2.getPrefixLength()
+ )
+
+ @Test
+ fun testNetworkWithIPv6LocalAddress_AddressAddedToBpfMap() {
+ val nr = nr(TRANSPORT_WIFI)
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(nr, cb)
+
+ // Connecting to network with IPv6 local address in LinkProperties
+ val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
+ 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)
+ )
+ }
+
+ @Test
+ fun testNetworkWithIPv4LocalAddress_AddressAddedToBpfMap() {
+ val nr = nr(TRANSPORT_WIFI)
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(nr, cb)
+
+ val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV4_LINK_ADDRRESS_1)
+ 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 IPv4 matching prefix(10.0.0.0/8) should be populated in local_net_access map
+ verify(bpfNetMaps).addLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV4 + 8),
+ eq(WIFI_IFNAME),
+ eq(InetAddresses.parseNumericAddress("10.0.0.0")),
+ eq(0),
+ eq(0),
+ eq(false)
+ )
+ }
+
+ @Test
+ fun testChangeLinkPropertiesWithDifferentLinkAddresses_AddressReplacedInBpfMap() {
+ val nr = nr(TRANSPORT_WIFI)
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(nr, cb)
+
+ val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
+ 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)
+ )
+
+ // Updating Link Property from IPv6 in Link Address to IPv4 in Link Address
+ val wifiLp2 = lp(WIFI_IFNAME, LOCAL_IPV4_LINK_ADDRRESS_1)
+ wifiAgent.sendLinkProperties(wifiLp2)
+ cb.expect<LinkPropertiesChanged>(wifiAgent.network)
+
+ // Verifying IPv4 matching prefix(10.0.0.0/8) should be populated in local_net_access map
+ verify(bpfNetMaps).addLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV4 + 8),
+ eq(WIFI_IFNAME),
+ eq(InetAddresses.parseNumericAddress("10.0.0.0")),
+ eq(0),
+ eq(0),
+ eq(false)
+ )
+ // Verifying IPv6 address should be removed from local_net_access map
+ verify(bpfNetMaps).removeLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+ eq(WIFI_IFNAME),
+ eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+ eq(0),
+ eq(0)
+ )
+ }
+
+ @Test
+ fun testAddingThenRemovingStackedLinkProperties_AddressAddedThenRemovedInBpfMap() {
+ 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())
+
+ // 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 testChangeStackedLinkProperties_AddressReplacedBpfMap() {
+ 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 multiple stacked links
+ val wifiLp_3 = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS_2)
+ val wifiLp_4 = lp(WIFI_IFNAME_2, LOCAL_IPV4_LINK_ADDRRESS_2)
+ val wifiLp_5 = lp(WIFI_IFNAME_3, LOCAL_IPV6_LINK_ADDRESS_3)
+ wifiLp_3.addStackedLink(wifiLp_4)
+ wifiLp_3.addStackedLink(wifiLp_5)
+ wifiAgent.sendLinkProperties(wifiLp_3)
+ cb.expect<LinkPropertiesChanged>(wifiAgent.network)
+
+ // Multicast and Broadcast address should always be populated on stacked link
+ // in local_net_access map
+ verifyPopulationOfMulticastAndBroadcastAddress(WIFI_IFNAME_3)
+ // Verifying new base IPv6 address should be populated in local_net_access map
+ verify(bpfNetMaps).addLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_2_PREFIX.getPrefixLength()),
+ eq(WIFI_IFNAME),
+ eq(LOCAL_IPV6_IP_ADDRESS_2_PREFIX.getAddress()),
+ eq(0),
+ eq(0),
+ eq(false)
+ )
+ // Verifying IPv4 matching prefix(10.0.0.0/8) should be populated as part of stacked link
+ // in local_net_access map
+ verify(bpfNetMaps, times(2)).addLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV4 + 8),
+ eq(WIFI_IFNAME_2),
+ eq(InetAddresses.parseNumericAddress("10.0.0.0")),
+ eq(0),
+ eq(0),
+ eq(false)
+ )
+ // Verifying newly stacked IPv6 address should be populated in local_net_access map
+ verify(bpfNetMaps).addLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_3_PREFIX.getPrefixLength()),
+ eq(WIFI_IFNAME_3),
+ eq(LOCAL_IPV6_IP_ADDRESS_3_PREFIX.getAddress()),
+ eq(0),
+ eq(0),
+ eq(false)
+ )
+ // Verifying old base IPv6 address should be removed from local_net_access map
+ verify(bpfNetMaps).removeLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+ eq(WIFI_IFNAME),
+ eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+ eq(0),
+ eq(0)
+ )
+ // As both stacked links is had same prefix, 10.0.0.0/8 should not be removed from
+ // local_net_access map.
+ verify(bpfNetMaps, never()).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()
+ cm.requestNetwork(nr, cb)
+
+ val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV4_LINK_ADDRRESS_1)
+ 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 IPv4 matching prefix(10.0.0.0/8) should be populated in local_net_access map
+ verify(bpfNetMaps).addLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV4 + 8),
+ eq(WIFI_IFNAME),
+ eq(LOCAL_IPV4_IP_ADDRESS_PREFIX_1.getAddress()),
+ eq(0),
+ eq(0),
+ eq(false)
+ )
+
+ // Updating Link Property from one IPv4 to another IPv4 within same range(10.0.0.0/8)
+ val wifiLp2 = lp(WIFI_IFNAME, LOCAL_IPV4_LINK_ADDRRESS_2)
+ wifiAgent.sendLinkProperties(wifiLp2)
+ cb.expect<LinkPropertiesChanged>(wifiAgent.network)
+
+ // As both addresses below to same range, so no address should be removed from the map.
+ verify(bpfNetMaps, never()).removeLocalNetAccess(any(), any(), any(), any(), any())
+ }
+
+ @Test
+ fun testChangeLinkPropertiesWithDifferentInterface_AddressReplacedInBpfMap() {
+ val nr = nr(TRANSPORT_WIFI)
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(nr, cb)
+
+ val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
+ 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)
+ )
+
+ // Updating Link Property by changing interface name which has IPv4 instead of IPv6
+ val wifiLp2 = lp(WIFI_IFNAME_2, LOCAL_IPV4_LINK_ADDRRESS_1)
+ wifiAgent.sendLinkProperties(wifiLp2)
+ cb.expect<LinkPropertiesChanged>(wifiAgent.network)
+
+ // Multicast and Broadcast address should be populated in local_net_access map for
+ // new interface
+ verifyPopulationOfMulticastAndBroadcastAddress(WIFI_IFNAME_2)
+ // Verifying IPv4 matching prefix(10.0.0.0/8) should be populated 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)
+ )
+ // Multicast and Broadcast address should be removed in local_net_access map for
+ // old interface
+ verifyRemovalOfMulticastAndBroadcastAddress()
+ // Verifying IPv6 address should be removed from local_net_access map
+ verify(bpfNetMaps).removeLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+ eq(WIFI_IFNAME),
+ eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+ eq(0),
+ eq(0)
+ )
+ }
+
+ @Test
+ fun testAddingAnotherNetwork_AllAddressesAddedInBpfMap() {
+ val nr = nr(TRANSPORT_WIFI)
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(nr, cb)
+
+ val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
+ 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)
+ )
+
+ // Adding another network with LinkProperty having IPv4 in LinkAddress
+ val wifiLp2 = lp(WIFI_IFNAME_2, LOCAL_IPV4_LINK_ADDRRESS_1)
+ val wifiAgent2 = Agent(nc = wifiNc, lp = wifiLp2)
+ wifiAgent2.connect()
+
+ // Multicast and Broadcast address should always be populated in local_net_access map
+ verifyPopulationOfMulticastAndBroadcastAddress(WIFI_IFNAME_2)
+ // Verifying IPv4 matching prefix(10.0.0.0/8) should be populated 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)
+ )
+ // Verifying nothing should be removed from local_net_access map
+ verify(bpfNetMaps, never()).removeLocalNetAccess(any(), any(), any(), any(), any())
+ }
+
+ @Test
+ fun testDestroyingNetwork_AddressesRemovedFromBpfMap() {
+ val nr = nr(TRANSPORT_WIFI)
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(nr, cb)
+
+ val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
+ 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)
+ )
+
+ // Unregistering the network
+ wifiAgent.unregisterAfterReplacement(LONG_TIMEOUT_MS)
+ cb.expect<Lost>(wifiAgent.network)
+
+ // Multicast and Broadcast address should be removed in local_net_access map for
+ // old interface
+ verifyRemovalOfMulticastAndBroadcastAddress()
+ // Verifying IPv6 address should be removed from local_net_access map
+ verify(bpfNetMaps).removeLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+ eq(WIFI_IFNAME),
+ eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+ eq(0),
+ eq(0)
+ )
+ }
+
+ // Verify if multicast and broadcast addresses have been added using addLocalNetAccess
+ fun verifyPopulationOfMulticastAndBroadcastAddress(
+ interfaceName: String = WIFI_IFNAME
+ ) {
+ verify(bpfNetMaps).addLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV4 + 4),
+ eq(interfaceName),
+ eq(InetAddresses.parseNumericAddress("224.0.0.0")),
+ eq(0),
+ eq(0),
+ eq(false)
+ )
+ verify(bpfNetMaps).addLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV6 + 8),
+ eq(interfaceName),
+ eq(InetAddresses.parseNumericAddress("ff00::")),
+ eq(0),
+ eq(0),
+ eq(false)
+ )
+ verify(bpfNetMaps).addLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV4 + 32),
+ eq(interfaceName),
+ eq(InetAddresses.parseNumericAddress("255.255.255.255")),
+ eq(0),
+ eq(0),
+ eq(false)
+ )
+ }
+
+ // Verify if multicast and broadcast addresses have been removed using removeLocalNetAccess
+ fun verifyRemovalOfMulticastAndBroadcastAddress(
+ interfaceName: String = WIFI_IFNAME
+ ) {
+ verify(bpfNetMaps).removeLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV4 + 4),
+ eq(interfaceName),
+ eq(InetAddresses.parseNumericAddress("224.0.0.0")),
+ eq(0),
+ eq(0)
+ )
+ verify(bpfNetMaps).removeLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV6 + 8),
+ eq(interfaceName),
+ eq(InetAddresses.parseNumericAddress("ff00::")),
+ eq(0),
+ eq(0)
+ )
+ verify(bpfNetMaps).removeLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV4 + 32),
+ eq(interfaceName),
+ eq(InetAddresses.parseNumericAddress("255.255.255.255")),
+ eq(0),
+ eq(0)
+ )
+ }
+}
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/ethernet/EthernetTrackerTest.java b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
index b7cfaf9..533bbf8 100644
--- a/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
+++ b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
@@ -79,6 +79,7 @@
initMockResources();
doReturn(false).when(mFactory).updateInterfaceLinkState(anyString(), anyBoolean());
doReturn(new String[0]).when(mNetd).interfaceGetList();
+ doReturn(new String[0]).when(mFactory).getAvailableInterfaces(anyBoolean());
mHandlerThread = new HandlerThread(THREAD_NAME);
mHandlerThread.start();
tracker = new EthernetTracker(mContext, mHandlerThread.getThreadHandler(), mFactory, mNetd,
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..7ebe384
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/HeaderCompressionUtilsTest.kt
@@ -0,0 +1,271 @@
+/*
+ * 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.internal.util.HexDump
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.google.common.truth.Truth.assertThat
+import java.io.IOException
+import java.nio.BufferUnderflowException
+import kotlin.test.assertFailsWith
+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 testHeaderDecompression_invalidPacket() {
+ // 1-byte packet
+ var input = "60"
+ assertFailsWith(BufferUnderflowException::class) { decompressHex(input) }
+
+ // Short packet -- incomplete header
+ // 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
+ assertFailsWith(BufferUnderflowException::class) { decompressHex(input) }
+
+ // Packet starts with 0b111 instead of 0b011
+ // TF: 11, NH: 0, HLIM: 11, CID: 0, SAC: 0, SAM: 10, M: 1, DAC: 0, DAM: 11
+ input = "fb2b" +
+ "44" + // next header
+ "1234" + // source
+ "89" + // dest
+ "abcdef01" // payload
+ assertFailsWith(IOException::class) { decompressHex(input) }
+
+ // Illegal option NH = 1. Note that the packet is not valid as the code should throw as soon
+ // as the illegal option is encountered.
+ // TF: 11, NH: 1, HLIM: 11, CID: 0, SAC: 0, SAM: 10, M: 1, DAC: 0, DAM: 11
+ input = "7f2b" +
+ "1234" + // source
+ "89" + // dest
+ "e0" // Hop-by-hop options NHC
+ assertFailsWith(IOException::class) { decompressHex(input) }
+
+ // Illegal option CID = 1.
+ // TF: 11, NH: 0, HLIM: 11, CID: 1, SAC: 0, SAM: 10, M: 1, DAC: 0, DAM: 11
+ input = "7bab00" +
+ "1234" + // source
+ "89" + // dest
+ "e0" // Hop-by-hop options NHC
+ assertFailsWith(IOException::class) { decompressHex(input) }
+
+ // Illegal option SAC = 1.
+ // TF: 11, NH: 0, HLIM: 11, CID: 0, SAC: 1, SAM: 10, M: 1, DAC: 0, DAM: 11
+ input = "7b6b" +
+ "1234" + // source
+ "89" + // dest
+ "e0" // Hop-by-hop options NHC
+ assertFailsWith(IOException::class) { decompressHex(input) }
+
+ // Illegal option DAC = 1.
+ // TF: 10, NH: 0, HLIM: 10, CID: 0, SAC: 0, SAM: 10, M: 0, DAC: 1, DAM: 10
+ input = "7226" +
+ "cc" + // traffic class
+ "43" + // next header
+ "1234" + // source
+ "abcd" + // dest
+ "abcdef" // payload
+ assertFailsWith(IOException::class) { decompressHex(input) }
+
+
+ // Unsupported option DAM = 11
+ // TF: 10, NH: 0, HLIM: 10, CID: 0, SAC: 0, SAM: 10, M: 0, DAC: 0, DAM: 11
+ input = "7223" +
+ "cc" + // traffic class
+ "43" + // next header
+ "1234" + // source
+ "abcdef" // payload
+ assertFailsWith(IOException::class) { decompressHex(input) }
+
+ // Unsupported option SAM = 11
+ // TF: 10, NH: 0, HLIM: 10, CID: 0, SAC: 0, SAM: 11, M: 0, DAC: 0, DAM: 10
+ input = "7232" +
+ "cc" + // traffic class
+ "43" + // next header
+ "abcd" + // dest
+ "abcdef" // payload
+ assertFailsWith(IOException::class) { decompressHex(input) }
+ }
+
+ @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
new file mode 100644
index 0000000..e261732
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/L2capPacketForwarderTest.kt
@@ -0,0 +1,248 @@
+/*
+ * 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.bluetooth.BluetoothSocket
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.ParcelFileDescriptor
+import android.system.Os
+import android.system.OsConstants.AF_UNIX
+import android.system.OsConstants.SHUT_RD
+import android.system.OsConstants.SHUT_WR
+import android.system.OsConstants.SOCK_SEQPACKET
+import android.system.OsConstants.SOL_SOCKET
+import android.system.OsConstants.SO_RCVTIMEO
+import android.system.OsConstants.SO_SNDTIMEO
+import android.system.StructTimeval
+import com.android.server.net.L2capPacketForwarder.BluetoothSocketWrapper
+import com.android.server.net.L2capPacketForwarder.FdWrapper
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.google.common.truth.Truth.assertThat
+import java.io.FileDescriptor
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
+import java.nio.ByteBuffer
+import kotlin.arrayOf
+import kotlin.random.Random
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+private const val TIMEOUT = 1000L
+
+@ConnectivityModuleTest
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class L2capPacketForwarderTest {
+ private lateinit var forwarder: L2capPacketForwarder
+ private val tunFds = arrayOf(FileDescriptor(), FileDescriptor())
+ private val l2capFds = arrayOf(FileDescriptor(), FileDescriptor())
+ private lateinit var l2capInputStream: BluetoothL2capInputStream
+ private lateinit var l2capOutputStream: BluetoothL2capOutputStream
+ @Mock private lateinit var bluetoothSocket: BluetoothSocket
+ @Mock private lateinit var callback: L2capPacketForwarder.ICallback
+
+ private val handlerThread = HandlerThread("L2capPacketForwarderTest thread").apply { start() }
+ private val handler = Handler(handlerThread.looper)
+
+ /** Imitates the behavior of an L2CAP BluetoothSocket */
+ private class BluetoothL2capInputStream(
+ val fd: FileDescriptor,
+ ) : InputStream() {
+ val l2capBuffer = ByteBuffer.wrap(ByteArray(0xffff)).apply {
+ limit(0)
+ }
+
+ override fun read(): Int {
+ throw NotImplementedError("b/391623333: not implemented correctly for L2cap sockets")
+ }
+
+ /** See BluetoothSocket#read(buf, off, len) */
+ override fun read(b: ByteArray, off: Int, len: Int): Int {
+ // If no more bytes are remaining, read from the fd into the intermediate buffer.
+ if (l2capBuffer.remaining() == 0) {
+ // fillL2capRxBuffer()
+ // refill buffer and return - 1
+ val backingArray = l2capBuffer.array()
+ var bytesRead = 0
+ try {
+ bytesRead = Os.read(fd, backingArray, 0 /*off*/, backingArray.size)
+ } catch (e: Exception) {
+ // read failed, timed out, or was interrupted
+ // InputStream throws IOException
+ throw IOException(e)
+ }
+ l2capBuffer.rewind()
+ l2capBuffer.limit(bytesRead)
+ }
+
+ val bytesToRead = if (len > l2capBuffer.remaining()) l2capBuffer.remaining() else len
+ l2capBuffer.get(b, off, bytesToRead)
+ return bytesToRead
+ }
+
+ override fun available(): Int {
+ throw NotImplementedError("b/391623333: not implemented correctly for L2cap sockets")
+ }
+
+ override fun close() {
+ try {
+ Os.shutdown(fd, SHUT_RD)
+ } catch (e: Exception) {
+ // InputStream throws IOException
+ throw IOException(e)
+ }
+ }
+ }
+
+ /** Imitates the behavior of an L2CAP BluetoothSocket */
+ private class BluetoothL2capOutputStream(
+ val fd: FileDescriptor,
+ ) : OutputStream() {
+
+ override fun write(b: Int) {
+ throw NotImplementedError("This method does not maintain packet boundaries, do not use")
+ }
+
+ /** See BluetoothSocket#write(buf, off, len) */
+ override fun write(b: ByteArray, off: Int, len: Int) {
+ try {
+ Os.write(fd, b, off, len)
+ } catch (e: Exception) {
+ // OutputStream throws IOException
+ throw IOException(e)
+ }
+ }
+
+ override fun close() {
+ try {
+ Os.shutdown(fd, SHUT_WR)
+ } catch (e: Exception) {
+ // OutputStream throws IOException
+ throw IOException(e)
+ }
+ }
+ }
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ Os.socketpair(AF_UNIX, SOCK_SEQPACKET, 0, tunFds[0], tunFds[1])
+ Os.socketpair(AF_UNIX, SOCK_SEQPACKET, 0, l2capFds[0], l2capFds[1])
+
+ // Set socket i/o timeout for test end.
+ Os.setsockoptTimeval(tunFds[1], SOL_SOCKET, SO_RCVTIMEO, StructTimeval.fromMillis(5000))
+ Os.setsockoptTimeval(tunFds[1], SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(5000))
+ Os.setsockoptTimeval(l2capFds[1], SOL_SOCKET, SO_RCVTIMEO, StructTimeval.fromMillis(5000))
+ Os.setsockoptTimeval(l2capFds[1], SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(5000))
+
+ l2capInputStream = BluetoothL2capInputStream(l2capFds[0])
+ l2capOutputStream = BluetoothL2capOutputStream(l2capFds[0])
+ doReturn(l2capInputStream).`when`(bluetoothSocket).getInputStream()
+ doReturn(l2capOutputStream).`when`(bluetoothSocket).getOutputStream()
+ doAnswer({
+ l2capInputStream.close()
+ l2capOutputStream.close()
+ try {
+ // libcore's Linux_close properly invalidates the FileDescriptor, so it is safe to
+ // close multiple times.
+ Os.close(l2capFds[0])
+ } catch (e: Exception) {
+ // BluetoothSocket#close can be called multiple times. Catch EBADF on subsequent
+ // invocations.
+ }
+ }).`when`(bluetoothSocket).close()
+
+ forwarder = L2capPacketForwarder(
+ handler,
+ FdWrapper(ParcelFileDescriptor(tunFds[0])),
+ BluetoothSocketWrapper(bluetoothSocket),
+ false /* compressHeaders */,
+ callback
+ )
+ }
+
+ @After
+ fun tearDown() {
+ if (::forwarder.isInitialized) {
+ // forwarder closes tunFds[0] and l2capFds[0]
+ forwarder.tearDown()
+ } else {
+ Os.close(tunFds[0])
+ Os.close(l2capFds[0])
+ }
+ Os.close(tunFds[1])
+ Os.close(l2capFds[1])
+
+ handlerThread.quitSafely()
+ handlerThread.join()
+ }
+
+ fun sendPacket(fd: FileDescriptor, size: Int = 1280): ByteArray {
+ val packet = ByteArray(size)
+ Random.nextBytes(packet)
+ Os.write(fd, packet, 0 /*off*/, packet.size)
+ return packet
+ }
+
+ fun assertPacketReceived(fd: FileDescriptor, expected: ByteArray) {
+ val readBuffer = ByteArray(expected.size)
+ Os.read(fd, readBuffer, 0 /*off*/, readBuffer.size)
+ assertThat(readBuffer).isEqualTo(expected)
+ }
+
+ @Test
+ fun testForwarding_withoutHeaderCompression() {
+ var packet = sendPacket(l2capFds[1])
+ var packet2 = sendPacket(l2capFds[1])
+ assertPacketReceived(tunFds[1], packet)
+ assertPacketReceived(tunFds[1], packet2)
+
+ packet = sendPacket(tunFds[1])
+ packet2 = sendPacket(tunFds[1])
+ assertPacketReceived(l2capFds[1], packet)
+ assertPacketReceived(l2capFds[1], packet2)
+ }
+
+ @Test
+ fun testForwarding_packetExceedsMtu() {
+ // Reading from tun drops packets that exceed MTU.
+ // drop
+ sendPacket(tunFds[1], L2capPacketForwarder.MTU + 1)
+ // drop
+ sendPacket(tunFds[1], L2capPacketForwarder.MTU + 42)
+ var packet = sendPacket(l2capFds[1], 1280)
+ assertPacketReceived(tunFds[1], packet)
+
+ // On the BluetoothSocket side, reads that exceed MTU stop forwarding.
+ sendPacket(l2capFds[1], L2capPacketForwarder.MTU + 1)
+ verify(callback, timeout(TIMEOUT)).onError()
+ }
+}
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/tests/unit/jni/Android.bp b/tests/unit/jni/Android.bp
index 1a833e1..1e9db03 100644
--- a/tests/unit/jni/Android.bp
+++ b/tests/unit/jni/Android.bp
@@ -22,6 +22,7 @@
],
shared_libs: [
+ "libbase",
"liblog",
"libnativehelper",
"libnetdutils",
diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp
index 2630d21..901dee7 100644
--- a/thread/tests/cts/Android.bp
+++ b/thread/tests/cts/Android.bp
@@ -51,7 +51,6 @@
libs: [
"android.test.base.stubs",
"android.test.runner.stubs",
- "framework-connectivity-module-api-stubs-including-flagged",
],
// Test coverage system runs on different devices. Need to
// compile for all architectures.
diff --git a/thread/tests/cts/AndroidTest.xml b/thread/tests/cts/AndroidTest.xml
index e954d3b..89d2ce5 100644
--- a/thread/tests/cts/AndroidTest.xml
+++ b/thread/tests/cts/AndroidTest.xml
@@ -57,13 +57,4 @@
<option name="exclude-annotation" value="org.junit.Ignore"/>
</test>
- <!--
- This doesn't override a read-only flag, to run the tests locally with `epskc_enabled` flag
- enabled, set the flag to `is_fixed_read_only: false`. This should be removed after the
- `epskc_enabled` flag is rolled out.
- -->
- <target_preparer class="com.android.tradefed.targetprep.FeatureFlagTargetPreparer">
- <option name="flag-value"
- value="thread_network/com.android.net.thread.flags.epskc_enabled=true"/>
- </target_preparer>
</configuration>
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
index 2d487ca..a979721 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -1296,6 +1296,7 @@
}
@Test
+ @Ignore("b/333649897, b/332195449: The 3 meshcop tests are flaky in different environments")
public void meshcopService_threadEnabledButNotJoined_discoveredButNoNetwork() throws Exception {
setUpTestNetwork();
@@ -1348,6 +1349,7 @@
}
@Test
+ @Ignore("b/333649897, b/332195449: The 3 meshcop tests are flaky in different environments")
public void meshcopService_threadDisabled_notDiscovered() throws Exception {
setUpTestNetwork();
CompletableFuture<NsdServiceInfo> serviceLostFuture = new CompletableFuture<>();
diff --git a/thread/tests/integration/src/android/net/thread/InternetAccessTest.kt b/thread/tests/integration/src/android/net/thread/InternetAccessTest.kt
index 3c9aa07..46d4708 100644
--- a/thread/tests/integration/src/android/net/thread/InternetAccessTest.kt
+++ b/thread/tests/integration/src/android/net/thread/InternetAccessTest.kt
@@ -167,6 +167,8 @@
val ftd = ftds[0]
joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET)
dnsServer.start()
+ ftd.autoStartSrpClient()
+ ftd.waitForSrpServer()
val ipv4Addresses =
ftd.resolveHost("google.com", TYPE_A).map { extractIpv4AddressFromMappedAddress(it) }
@@ -181,6 +183,8 @@
val ftd = ftds[0]
joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET)
dnsServer.start()
+ ftd.autoStartSrpClient()
+ ftd.waitForSrpServer()
assertThat(ftd.resolveHost("google.com", TYPE_A)).isEmpty()
assertThat(ftd.resolveHost("google.com", TYPE_AAAA)).isEmpty()
diff --git a/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
index 6c2a9bb..f959ccf 100644
--- a/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
+++ b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
@@ -113,8 +113,8 @@
@Before
public void setUp() throws Exception {
- mOtCtl.factoryReset();
mController.setEnabledAndWait(true);
+ mController.leaveAndWait();
mController.joinAndWait(DEFAULT_DATASET);
mNsdManager = mContext.getSystemService(NsdManager.class);
diff --git a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
index d41550b..2641a77 100644
--- a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
+++ b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
@@ -130,11 +130,8 @@
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
- mOtCtl.factoryReset();
-
mFtd = new FullThreadDevice(10 /* nodeId */);
}
@@ -351,7 +348,6 @@
mOtCtl.executeCommand("netdata register");
mController.leaveAndWait();
- mOtCtl.factoryReset();
mController.joinAndWait(DEFAULT_DATASET);
LinkProperties lp = cm.getLinkProperties(getThreadNetwork(CALLBACK_TIMEOUT));
diff --git a/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java b/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java
index 2f0ab34..ac688dd 100644
--- a/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java
+++ b/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java
@@ -66,11 +66,7 @@
@Before
public void setUp() throws Exception {
- // TODO(b/366141754): The current implementation of "thread_network ot-ctl factoryreset"
- // results in timeout error.
- // A future fix will provide proper support for factoryreset, allowing us to replace the
- // legacy "ot-ctl".
- mOtCtl.factoryReset();
+ mController.leaveAndWait();
mFtd = new FullThreadDevice(10 /* nodeId */);
ensureThreadEnabled();
diff --git a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
index 209eed6..38961a3 100644
--- a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
+++ b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
@@ -521,7 +521,7 @@
}
/** Waits for an SRP server to be present in Network Data */
- private void waitForSrpServer() throws TimeoutException {
+ public void waitForSrpServer() throws TimeoutException {
// CLI output:
// > srp client server
// [fd64:db12:25f4:7e0b:1bfc:6344:25ac:2dd7]:53538
diff --git a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
index 801e21e..f00c9cd 100644
--- a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
+++ b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
@@ -603,11 +603,12 @@
/** Enables Thread and joins the specified Thread network. */
@JvmStatic
fun enableThreadAndJoinNetwork(dataset: ActiveOperationalDataset) {
- // TODO: b/323301831 - This is a workaround to avoid unnecessary delay to re-form a network
- OtDaemonController().factoryReset();
-
val context: Context = requireNotNull(ApplicationProvider.getApplicationContext());
val controller = requireNotNull(ThreadNetworkControllerWrapper.newInstance(context));
+
+ // TODO: b/323301831 - This is a workaround to avoid unnecessary delay to re-form a network
+ controller.leaveAndWait();
+
controller.setEnabledAndWait(true);
controller.joinAndWait(dataset);
}
diff --git a/thread/tests/multidevices/Android.bp b/thread/tests/multidevices/Android.bp
index 050caa8..1d2ae62 100644
--- a/thread/tests/multidevices/Android.bp
+++ b/thread/tests/multidevices/Android.bp
@@ -35,9 +35,4 @@
"mts-tethering",
"general-tests",
],
- version: {
- py3: {
- embedded_launcher: true,
- },
- },
}