Merge "Ensure APF counter increased for testDropPingReply() and testReplyPing()" into main
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/res/values-iw/strings.xml b/Tethering/res/values-iw/strings.xml
index f7fb4d5..b1c0a9c 100644
--- a/Tethering/res/values-iw/strings.xml
+++ b/Tethering/res/values-iw/strings.xml
@@ -17,7 +17,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="tethered_notification_title" msgid="5350162111436634622">"שיתוף האינטרנט או הנקודה לשיתוף אינטרנט פעילים"</string>
- <string name="tethered_notification_message" msgid="2338023450330652098">"יש להקיש כדי להגדיר."</string>
+ <string name="tethered_notification_message" msgid="2338023450330652098">"יש ללחוץ כדי להגדיר."</string>
<string name="disable_tether_notification_title" msgid="3183576627492925522">"שיתוף האינטרנט בין מכשירים מושבת"</string>
<string name="disable_tether_notification_message" msgid="6655882039707534929">"לפרטים, יש לפנות לאדמין"</string>
<string name="notification_channel_tethering_status" msgid="7030733422705019001">"סטטוס של נקודה לשיתוף אינטרנט ושיתוף אינטרנט בין מכשירים"</string>
diff --git a/Tethering/res/values-nb/strings.xml b/Tethering/res/values-nb/strings.xml
index e9024c0..fe91b82 100644
--- a/Tethering/res/values-nb/strings.xml
+++ b/Tethering/res/values-nb/strings.xml
@@ -17,7 +17,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="tethered_notification_title" msgid="5350162111436634622">"Internettdeling eller wifi-sone er aktiv"</string>
- <string name="tethered_notification_message" msgid="2338023450330652098">"Trykk for å konfigurere."</string>
+ <string name="tethered_notification_message" msgid="2338023450330652098">"Konfigurer."</string>
<string name="disable_tether_notification_title" msgid="3183576627492925522">"Internettdeling er slått av"</string>
<string name="disable_tether_notification_message" msgid="6655882039707534929">"Kontakt administratoren din for å få mer informasjon"</string>
<string name="notification_channel_tethering_status" msgid="7030733422705019001">"Status for wifi-sone og internettdeling"</string>
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index 609d759..d0ba431 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -852,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) {
@@ -888,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.
@@ -901,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.
@@ -921,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);
}
}
}
@@ -1126,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;
@@ -1148,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);
@@ -1227,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/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 b50831d..1589509 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -29,7 +29,6 @@
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_GLOBAL;
-import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
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;
@@ -78,6 +77,9 @@
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;
@@ -241,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> mPendingTetheringRequests =
- new SparseArray<>();
+ private final RequestTracker mRequestTracker;
private final Context mContext;
private final ArrayMap<String, TetherState> mTetherStates;
@@ -308,6 +306,7 @@
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
@@ -704,14 +703,13 @@
final IIntResultListener listener) {
mHandler.post(() -> {
final int type = request.getTetheringType();
- final TetheringRequest unfinishedRequest = mPendingTetheringRequests.get(type);
- // If tethering is already enabled with a different request,
- // disable before re-enabling.
- if (unfinishedRequest != null && !unfinishedRequest.equalsIgnoreUidPackage(request)) {
- enableTetheringInternal(false /* disabled */, unfinishedRequest, 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);
}
- mPendingTetheringRequests.put(type, request);
if (request.isExemptFromEntitlementCheck()) {
mEntitlementMgr.setExemptedDownstreamType(type);
@@ -731,9 +729,7 @@
}
private boolean isTetheringTypePendingOrServing(final int type) {
- for (int i = 0; i < mPendingTetheringRequests.size(); i++) {
- if (mPendingTetheringRequests.valueAt(i).getTetheringType() == type) return true;
- }
+ 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
@@ -764,7 +760,7 @@
}
void stopTetheringInternal(int type) {
- mPendingTetheringRequests.remove(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.
@@ -822,7 +818,7 @@
// If changing tethering fail, remove corresponding request
// no matter who trigger the start/stop.
if (result != TETHER_ERROR_NO_ERROR) {
- mPendingTetheringRequests.remove(type);
+ mRequestTracker.removeAllPendingRequests(type);
mTetheringMetrics.updateErrorCode(type, result);
mTetheringMetrics.sendReport(type);
}
@@ -991,7 +987,7 @@
if (this != mBluetoothCallback) return;
final TetheringRequest request =
- getOrCreatePendingTetheringRequest(TETHERING_BLUETOOTH);
+ mRequestTracker.getOrCreatePendingRequest(TETHERING_BLUETOOTH);
enableIpServing(request, iface);
mConfiguredBluetoothIface = iface;
}
@@ -1048,7 +1044,8 @@
return;
}
- final TetheringRequest request = getOrCreatePendingTetheringRequest(TETHERING_ETHERNET);
+ final TetheringRequest request = mRequestTracker.getOrCreatePendingRequest(
+ TETHERING_ETHERNET);
enableIpServing(request, iface);
mConfiguredEthernetIface = iface;
}
@@ -1080,61 +1077,6 @@
return TETHER_ERROR_NO_ERROR;
}
- /**
- * 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.
- */
- private 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
- private 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
- private TetheringRequest createPlaceholderRequest(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;
- }
-
- /**
- * Gets the TetheringRequest that #startTethering was called with but is waiting for the link
- * layer event to indicate the interface is available to tether.
- * 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
- private TetheringRequest getOrCreatePendingTetheringRequest(int type) {
- TetheringRequest pending = mPendingTetheringRequests.get(type);
- if (pending != null) return pending;
-
- Log.w(TAG, "No pending TetheringRequest for type " + type + " found, creating a placeholder"
- + " request");
- return createPlaceholderRequest(type);
- }
-
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
@@ -1152,7 +1094,10 @@
} catch (RemoteException e) { }
}
- final TetheringRequest request = createLegacyGlobalScopeTetheringRequest(type);
+ TetheringRequest request = mRequestTracker.getNextPendingRequest(type);
+ if (request == null) {
+ request = createLegacyGlobalScopeTetheringRequest(type);
+ }
int result = tetherInternal(request, iface);
switch (type) {
case TETHERING_WIFI:
@@ -1222,7 +1167,7 @@
// 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.
- mPendingTetheringRequests.remove(request.getTetheringType());
+ mRequestTracker.removeAllPendingRequests(request.getTetheringType());
tetherState.ipServer.enable(request);
if (request.getRequestType() == REQUEST_TYPE_PLACEHOLDER) {
TerribleErrorLog.logTerribleError(TetheringStatsLog::write,
@@ -1588,8 +1533,8 @@
}
@VisibleForTesting
- SparseArray<TetheringRequest> getPendingTetheringRequests() {
- return mPendingTetheringRequests;
+ List<TetheringRequest> getPendingTetheringRequests() {
+ return mRequestTracker.getPendingTetheringRequests();
}
@VisibleForTesting
@@ -1649,10 +1594,6 @@
}
}
- final TetheringRequest getPendingTetheringRequest(int type) {
- return mPendingTetheringRequests.get(type, null);
- }
-
private void enableIpServing(@NonNull TetheringRequest request, String ifname) {
enableIpServing(request, ifname, false /* isNcm */);
}
@@ -1740,7 +1681,7 @@
switch (wifiIpMode) {
case IFACE_IP_MODE_TETHERED:
type = maybeInferWifiTetheringType(ifname);
- request = getOrCreatePendingTetheringRequest(type);
+ 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
@@ -1795,7 +1736,7 @@
return;
}
- final TetheringRequest request = getOrCreatePendingTetheringRequest(tetheringType);
+ final TetheringRequest request = mRequestTracker.getOrCreatePendingRequest(tetheringType);
if (ifaces != null) {
for (String iface : ifaces) {
if (ifaceNameToType(iface) == tetheringType) {
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/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/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/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
index e1c2db9..51efaf8 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -964,7 +964,7 @@
mLooper.dispatchAll();
assertEquals(1, mTethering.getPendingTetheringRequests().size());
- assertEquals(request, mTethering.getPendingTetheringRequests().get(TETHERING_USB));
+ assertTrue(mTethering.getPendingTetheringRequests().get(0).equals(request));
if (mTethering.getTetheringConfiguration().isUsingNcm()) {
verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_NCM);
@@ -2879,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);
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_helpers.h b/bpf/headers/include/bpf_helpers.h
index 6a0e5a8..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)
diff --git a/bpf/headers/include/bpf_map_def.h b/bpf/headers/include/bpf_map_def.h
index e95ca5f..2e5afca 100644
--- a/bpf/headers/include/bpf_map_def.h
+++ b/bpf/headers/include/bpf_map_def.h
@@ -163,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 9486e75..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,7 @@
#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;
@@ -122,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).
@@ -1187,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);
@@ -1216,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
@@ -1412,6 +1415,13 @@
}
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+
const int first_api_level = GetIntProperty("ro.board.first_api_level", api_level);
@@ -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;
}
@@ -1622,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.");
@@ -1654,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;
@@ -1688,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 e3e508b..d41aa81 100644
--- a/bpf/netd/BpfHandler.cpp
+++ b/bpf/netd/BpfHandler.cpp
@@ -341,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
@@ -360,15 +360,19 @@
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) {
- ALOGV("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);
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/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/offload.c b/bpf/progs/offload.c
index 0f23844..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"
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/tests/mts/bpf_existence_test.cpp b/bpf/tests/mts/bpf_existence_test.cpp
index 75fb8e9..4d5f9b5 100644
--- a/bpf/tests/mts/bpf_existence_test.cpp
+++ b/bpf/tests/mts/bpf_existence_test.cpp
@@ -196,7 +196,12 @@
// 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);
+
+ // 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.
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/framework/Android.bp b/framework/Android.bp
index f66bc60..ab3af9a 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -295,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: [
@@ -308,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) " +
@@ -320,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/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/connectivity/ConnectivityCompatChanges.java b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
index 317854b..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;
/**
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/CertificateTransparencyJob.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
index e6f1379..f1b9a4f 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
@@ -38,6 +38,7 @@
private final DataStore mDataStore;
private final CertificateTransparencyDownloader mCertificateTransparencyDownloader;
private final CompatibilityVersion mCompatVersion;
+ private final SignatureVerifier mSignatureVerifier;
private final AlarmManager mAlarmManager;
private final PendingIntent mPendingIntent;
@@ -49,11 +50,13 @@
Context context,
DataStore dataStore,
CertificateTransparencyDownloader certificateTransparencyDownloader,
- CompatibilityVersion compatVersion) {
+ CompatibilityVersion compatVersion,
+ SignatureVerifier signatureVerifier) {
mContext = context;
mDataStore = dataStore;
mCertificateTransparencyDownloader = certificateTransparencyDownloader;
mCompatVersion = compatVersion;
+ mSignatureVerifier = signatureVerifier;
mAlarmManager = context.getSystemService(AlarmManager.class);
mPendingIntent =
@@ -127,6 +130,7 @@
private void startDependencies() {
mDataStore.load();
mCertificateTransparencyDownloader.addCompatibilityVersion(mCompatVersion);
+ mSignatureVerifier.loadAllowedKeys();
mContext.registerReceiver(
mCertificateTransparencyDownloader,
new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
@@ -139,6 +143,7 @@
private void stopDependencies() {
mContext.unregisterReceiver(mCertificateTransparencyDownloader);
+ mSignatureVerifier.clearAllowedKeys();
mCertificateTransparencyDownloader.clearCompatibilityVersions();
mDataStore.delete();
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 a71ff7c..2e910b2 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
@@ -52,6 +52,7 @@
public CertificateTransparencyService(Context context) {
DataStore dataStore = new DataStore(Config.PREFERENCES_FILE);
+ SignatureVerifier signatureVerifier = new SignatureVerifier(context);
mCertificateTransparencyJob =
new CertificateTransparencyJob(
context,
@@ -60,13 +61,14 @@
context,
dataStore,
new DownloadHelper(context),
- new SignatureVerifier(context),
+ signatureVerifier,
new CertificateTransparencyLoggerImpl(dataStore)),
new CompatibilityVersion(
Config.COMPATIBILITY_VERSION,
Config.URL_SIGNATURE,
Config.URL_LOG_LIST,
- Config.CT_ROOT_DIRECTORY_PATH));
+ Config.CT_ROOT_DIRECTORY_PATH),
+ signatureVerifier);
}
/**
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 6040ef6..87a4973 100644
--- a/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
+++ b/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
@@ -30,6 +30,9 @@
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;
@@ -39,21 +42,39 @@
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 final Context mContext;
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;
@@ -82,7 +103,11 @@
}
@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);
}
@@ -105,21 +130,18 @@
byte[] signatureBytes = signatureStream.readAllBytes();
statusBuilder.setSignature(new String(signatureBytes));
- try {
- byte[] decodedSigBytes = Base64.getDecoder().decode(signatureBytes);
- if (!verifier.verify(decodedSigBytes)) {
- // 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();
+ 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, "Signature invalid for log list verification", e);
+ Log.e(TAG, "Key invalid for log list verification", e);
statusBuilder.setState(SIGNATURE_INVALID);
return statusBuilder.build();
} catch (IOException | GeneralSecurityException e) {
@@ -135,4 +157,9 @@
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 2af0122..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
@@ -109,6 +109,7 @@
mContext.getFilesDir());
prepareDownloadManager();
+ mSignatureVerifier.addAllowedKey(mPublicKey);
mDataStore.load();
mCertificateTransparencyDownloader.addCompatibilityVersion(mCompatVersion);
}
@@ -165,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();
@@ -197,8 +214,7 @@
}
@Test
- public void testDownloader_publicKeyDownloadFail_logsFailure()
- throws Exception {
+ public void testDownloader_publicKeyDownloadFail_logsFailure() throws Exception {
mCertificateTransparencyDownloader.startPublicKeyDownload();
mCertificateTransparencyDownloader.onReceive(
@@ -243,8 +259,7 @@
}
@Test
- public void testDownloader_metadataDownloadFail_logsFailure()
- throws Exception {
+ public void testDownloader_metadataDownloadFail_logsFailure() throws Exception {
mCertificateTransparencyDownloader.startMetadataDownload();
mCertificateTransparencyDownloader.onReceive(
@@ -294,8 +309,7 @@
}
@Test
- public void testDownloader_contentDownloadFail_logsFailure()
- throws Exception {
+ public void testDownloader_contentDownloadFail_logsFailure() throws Exception {
mCertificateTransparencyDownloader.startContentDownload(mCompatVersion);
mCertificateTransparencyDownloader.onReceive(
@@ -329,9 +343,8 @@
}
@Test
- public void
- testDownloader_contentDownloadSuccess_noPublicKeyFound_logsSingleFailure()
- throws Exception {
+ public void testDownloader_contentDownloadSuccess_noPublicKeyFound_logsSingleFailure()
+ throws Exception {
File logListFile = makeLogListFile("456");
File metadataFile = sign(logListFile);
mSignatureVerifier.setPublicKey(mPublicKey);
@@ -351,16 +364,17 @@
}
@Test
- public void
- testDownloader_contentDownloadSuccess_wrongSignatureAlgo_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());
+ PublicKey wrongAlgorithmKey =
+ KeyPairGenerator.getInstance("EC").generateKeyPair().getPublic();
+ mSignatureVerifier.addAllowedKey(wrongAlgorithmKey);
+ mSignatureVerifier.setPublicKey(wrongAlgorithmKey);
// Act
mCertificateTransparencyDownloader.startMetadataDownload();
@@ -377,16 +391,15 @@
}
@Test
- public void
- testDownloader_contentDownloadSuccess_signatureNotVerified_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());
+ File metadataFile = sign(logListFile, instance.generateKeyPair().getPrivate());
// Act
mCertificateTransparencyDownloader.startMetadataDownload();
@@ -405,9 +418,7 @@
}
@Test
- public void
- testDownloader_contentDownloadSuccess_installFail_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);
@@ -615,9 +626,14 @@
}
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)) {
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/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 c3306bd..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(
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/ServiceConnectivityResources/res/raw/ct_public_keys.pem b/service/ServiceConnectivityResources/res/raw/ct_public_keys.pem
index 80dccbe..8a5ebbf 100644
--- a/service/ServiceConnectivityResources/res/raw/ct_public_keys.pem
+++ b/service/ServiceConnectivityResources/res/raw/ct_public_keys.pem
@@ -1,4 +1,18 @@
-----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
diff --git a/service/ServiceConnectivityResources/res/values-iw/strings.xml b/service/ServiceConnectivityResources/res/values-iw/strings.xml
index b5b4071..88b539a 100644
--- a/service/ServiceConnectivityResources/res/values-iw/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-iw/strings.xml
@@ -23,15 +23,15 @@
<!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
<skip />
<string name="mobile_network_available_no_internet" msgid="1000871587359324217">"אין אינטרנט"</string>
- <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"יכול להיות שחבילת הגלישה שלך אצל <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> נגמרה. אפשר להקיש כדי להציג את האפשרויות."</string>
- <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"יכול להיות שחבילת הגלישה נגמרה. אפשר להקיש כדי להציג את האפשרויות."</string>
+ <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"יכול להיות שחבילת הגלישה שלך אצל <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> נגמרה. אפשר ללחוץ כדי להציג את האפשרויות."</string>
+ <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"יכול להיות שחבילת הגלישה נגמרה. אפשר ללחוץ כדי להציג את האפשרויות."</string>
<string name="wifi_no_internet" msgid="1326348603404555475">"ל-<xliff:g id="NETWORK_SSID">%1$s</xliff:g> אין גישה לאינטרנט"</string>
- <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"הקש לקבלת אפשרויות"</string>
+ <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"אפשר ללחוץ כדי להציג את האפשרויות"</string>
<string name="mobile_no_internet" msgid="4087718456753201450">"לרשת הסלולרית אין גישה לאינטרנט"</string>
<string name="other_networks_no_internet" msgid="5693932964749676542">"לרשת אין גישה לאינטרנט"</string>
<string name="private_dns_broken_detailed" msgid="2677123850463207823">"לא ניתן לגשת לשרת DNS הפרטי"</string>
<string name="network_partial_connectivity" msgid="5549503845834993258">"הקישוריות של <xliff:g id="NETWORK_SSID">%1$s</xliff:g> מוגבלת"</string>
- <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"כדי להתחבר למרות זאת יש להקיש"</string>
+ <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"כדי להתחבר למרות זאת יש ללחוץ"</string>
<string name="network_switch_metered" msgid="5016937523571166319">"מעבר אל <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
<string name="network_switch_metered_detail" msgid="1257300152739542096">"המכשיר משתמש ברשת <xliff:g id="NEW_NETWORK">%1$s</xliff:g> כשלרשת <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> אין גישה לאינטרנט. עשויים לחול חיובים."</string>
<string name="network_switch_metered_toast" msgid="70691146054130335">"עבר מרשת <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> לרשת <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
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/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 2a66e4b..b2e49e7 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -5405,12 +5405,12 @@
}
@VisibleForTesting
- protected static boolean shouldCreateNetworksImmediately() {
+ protected static boolean shouldCreateNetworksImmediately(@NonNull NetworkCapabilities caps) {
// The feature of creating the networks immediately was slated for U, but race conditions
// detected late required this was flagged off.
// TODO : enable this in a Mainline update or in V, and re-enable the test for this
// in NetworkAgentTest.
- return false;
+ return caps.hasCapability(NET_CAPABILITY_LOCAL_NETWORK);
}
private static boolean shouldCreateNativeNetwork(@NonNull NetworkAgentInfo nai,
@@ -5419,12 +5419,12 @@
if (state == NetworkInfo.State.CONNECTED) return true;
if (state != NetworkInfo.State.CONNECTING) {
// TODO: throw if no WTFs are observed in the field.
- if (shouldCreateNetworksImmediately()) {
+ if (shouldCreateNetworksImmediately(nai.getCapsNoCopy())) {
Log.wtf(TAG, "Uncreated network in invalid state: " + state);
}
return false;
}
- return nai.isVPN() || shouldCreateNetworksImmediately();
+ return nai.isVPN() || shouldCreateNetworksImmediately(nai.getCapsNoCopy());
}
private static boolean shouldDestroyNativeNetwork(@NonNull NetworkAgentInfo nai) {
@@ -5823,7 +5823,7 @@
}
if (shouldTrackUidsForBlockedStatusCallbacks()
- && isAppRequest(nri)
+ && nri.mMessenger != null
&& !nri.mUidTrackedForBlockedStatus) {
Log.wtf(TAG, "Registered nri is not tracked for sending blocked status: " + nri);
}
@@ -9810,8 +9810,8 @@
}
// The both list contain current link properties + stacked links for new and old LP.
- List<LinkProperties> newLinkProperties = new ArrayList<>();
- List<LinkProperties> oldLinkProperties = new ArrayList<>();
+ final List<LinkProperties> newLinkProperties = new ArrayList<>();
+ final List<LinkProperties> oldLinkProperties = new ArrayList<>();
if (newLp != null) {
newLinkProperties.add(newLp);
@@ -9824,13 +9824,13 @@
// map contains interface name to list of local network prefixes added because of change
// in link properties
- Map<String, List<IpPrefix>> prefixesAddedForInterface = new ArrayMap<>();
+ final Map<String, List<IpPrefix>> prefixesAddedForInterface = new ArrayMap<>();
final CompareResult<LinkProperties> linkPropertiesDiff = new CompareResult<>(
oldLinkProperties, newLinkProperties);
for (LinkProperties linkProperty : linkPropertiesDiff.added) {
- List<IpPrefix> unicastLocalPrefixesToBeAdded = new ArrayList<>();
+ final List<IpPrefix> unicastLocalPrefixesToBeAdded = new ArrayList<>();
for (LinkAddress linkAddress : linkProperty.getLinkAddresses()) {
unicastLocalPrefixesToBeAdded.addAll(
getLocalNetworkPrefixesForAddress(linkAddress));
@@ -9838,7 +9838,7 @@
addLocalAddressesToBpfMap(linkProperty.getInterfaceName(),
unicastLocalPrefixesToBeAdded, linkProperty);
- // adding iterface name -> ip prefixes that we added to map
+ // populating interface name -> ip prefixes which were added to local_net_access map.
if (!prefixesAddedForInterface.containsKey(linkProperty.getInterfaceName())) {
prefixesAddedForInterface.put(linkProperty.getInterfaceName(), new ArrayList<>());
}
@@ -9847,9 +9847,9 @@
}
for (LinkProperties linkProperty : linkPropertiesDiff.removed) {
- List<IpPrefix> unicastLocalPrefixesToBeRemoved = new ArrayList<>();
- List<IpPrefix> unicastLocalPrefixesAdded = prefixesAddedForInterface.getOrDefault(
- linkProperty.getInterfaceName(), new ArrayList<>());
+ final List<IpPrefix> unicastLocalPrefixesToBeRemoved = new ArrayList<>();
+ final List<IpPrefix> unicastLocalPrefixesAdded = prefixesAddedForInterface.getOrDefault(
+ linkProperty.getInterfaceName(), Collections.emptyList());
for (LinkAddress linkAddress : linkProperty.getLinkAddresses()) {
unicastLocalPrefixesToBeRemoved.addAll(
@@ -9857,8 +9857,8 @@
}
// This is to ensure if 10.0.10.0/24 was added and 10.0.11.0/24 was removed both will
- // still populate the same prefix of 10.0.0.0/8, which mean we should not remove the
- // prefix because of removal of 10.0.11.0/24
+ // 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(),
@@ -12048,7 +12048,7 @@
// interfaces and routing rules have been added, DNS servers programmed, etc.
// For VPNs, this must be done before the capabilities are updated, because as soon as
// that happens, UIDs are routed to the network.
- if (shouldCreateNetworksImmediately()) {
+ if (shouldCreateNetworksImmediately(networkAgent.getCapsNoCopy())) {
applyInitialLinkProperties(networkAgent);
}
@@ -12073,7 +12073,7 @@
networkAgent.getAndSetNetworkCapabilities(networkAgent.networkCapabilities);
handlePerNetworkPrivateDnsConfig(networkAgent, mDnsManager.getPrivateDnsConfig());
- if (!shouldCreateNetworksImmediately()) {
+ if (!shouldCreateNetworksImmediately(networkAgent.getCapsNoCopy())) {
applyInitialLinkProperties(networkAgent);
} else {
// The network was created when the agent registered, and the LinkProperties are
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/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/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/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/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/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/VpnTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
index 3430196..6c92b74 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
@@ -28,6 +28,7 @@
import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND;
import static android.net.ConnectivityManager.TYPE_VPN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
import static android.os.Process.INVALID_UID;
@@ -1024,7 +1025,10 @@
checkTrafficOnVpn();
final Network vpnNetwork = mCM.getActiveNetwork();
- myUidCallback.expectAvailableThenValidatedCallbacks(vpnNetwork, TIMEOUT_MS);
+ myUidCallback.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED,
+ NETWORK_CALLBACK_TIMEOUT_MS,
+ entry -> entry.getNetwork().equals(vpnNetwork)
+ && entry.getCaps().hasCapability(NET_CAPABILITY_VALIDATED));
assertEquals(vpnNetwork, mCM.getActiveNetwork());
assertNotEqual(defaultNetwork, vpnNetwork);
maybeExpectVpnTransportInfo(vpnNetwork);
@@ -1843,11 +1847,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 +1865,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 +1881,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 +1893,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/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/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 7292c5d..87c2b9e 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -1231,6 +1231,10 @@
* of a {@code NetworkCallback}.
*/
@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() {
final ConditionVariable received = new ConditionVariable();
@@ -1273,6 +1277,8 @@
// 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()
@@ -1400,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 */);
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 75b2814..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
@@ -160,11 +160,15 @@
@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);
}
/**
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 19a41d8..c4944b6 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -4040,7 +4040,7 @@
mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, callbacks);
- if (mService.shouldCreateNetworksImmediately()) {
+ if (mService.shouldCreateNetworksImmediately(mWiFiAgent.getNetworkCapabilities())) {
assertEquals("onNetworkCreated", eventOrder.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS));
} else {
assertNull(eventOrder.poll());
@@ -4053,7 +4053,7 @@
// connected.
// TODO: fix this bug, file the request before connecting, and remove the waitForIdle.
mWiFiAgent.connectWithoutInternet();
- if (!mService.shouldCreateNetworksImmediately()) {
+ if (!mService.shouldCreateNetworksImmediately(mWiFiAgent.getNetworkCapabilities())) {
assertEquals("onNetworkCreated", eventOrder.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS));
} else {
waitForIdle();
@@ -7941,8 +7941,8 @@
// Simple connection with initial LP should have updated ifaces.
mCellAgent.connect(false);
waitForIdle();
- List<Network> allNetworks = mService.shouldCreateNetworksImmediately()
- ? cellAndWifi() : onlyCell();
+ List<Network> allNetworks = mService.shouldCreateNetworksImmediately(
+ mCellAgent.getNetworkCapabilities()) ? cellAndWifi() : onlyCell();
expectNotifyNetworkStatus(allNetworks, onlyCell(), MOBILE_IFNAME);
reset(mStatsManager);
@@ -8254,7 +8254,7 @@
mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
final int netId = mCellAgent.getNetwork().netId;
waitForIdle();
- if (mService.shouldCreateNetworksImmediately()) {
+ if (mService.shouldCreateNetworksImmediately(mCellAgent.getNetworkCapabilities())) {
verify(mMockDnsResolver, times(1)).createNetworkCache(netId);
} else {
verify(mMockDnsResolver, never()).setResolverConfiguration(any());
@@ -8274,7 +8274,7 @@
mCellAgent.sendLinkProperties(cellLp);
mCellAgent.connect(false);
waitForIdle();
- if (!mService.shouldCreateNetworksImmediately()) {
+ if (!mService.shouldCreateNetworksImmediately(mCellAgent.getNetworkCapabilities())) {
// CS tells dnsresolver about the empty DNS config for this network.
verify(mMockDnsResolver, times(1)).createNetworkCache(netId);
}
@@ -8397,7 +8397,7 @@
mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
final int netId = mCellAgent.getNetwork().netId;
waitForIdle();
- if (mService.shouldCreateNetworksImmediately()) {
+ if (mService.shouldCreateNetworksImmediately(mCellAgent.getNetworkCapabilities())) {
verify(mMockDnsResolver, times(1)).createNetworkCache(netId);
} else {
verify(mMockDnsResolver, never()).setResolverConfiguration(any());
@@ -8420,7 +8420,7 @@
mCellAgent.sendLinkProperties(cellLp);
mCellAgent.connect(false);
waitForIdle();
- if (!mService.shouldCreateNetworksImmediately()) {
+ if (!mService.shouldCreateNetworksImmediately(mCellAgent.getNetworkCapabilities())) {
verify(mMockDnsResolver, times(1)).createNetworkCache(netId);
}
verify(mMockDnsResolver, atLeastOnce()).setResolverConfiguration(
@@ -16065,13 +16065,13 @@
final TestNetworkAgentWrapper workAgent =
makeEnterpriseNetworkAgent(profileNetworkPreference.getPreferenceEnterpriseId());
- if (mService.shouldCreateNetworksImmediately()) {
+ if (mService.shouldCreateNetworksImmediately(workAgent.getNetworkCapabilities())) {
expectNativeNetworkCreated(workAgent.getNetwork().netId, INetd.PERMISSION_SYSTEM,
null /* iface */, inOrder);
}
if (connectWorkProfileAgentAhead) {
workAgent.connect(false);
- if (!mService.shouldCreateNetworksImmediately()) {
+ if (!mService.shouldCreateNetworksImmediately(workAgent.getNetworkCapabilities())) {
expectNativeNetworkCreated(workAgent.getNetwork().netId, INetd.PERMISSION_SYSTEM,
null /* iface */, inOrder);
}
@@ -16114,7 +16114,7 @@
if (!connectWorkProfileAgentAhead) {
workAgent.connect(false);
- if (!mService.shouldCreateNetworksImmediately()) {
+ if (!mService.shouldCreateNetworksImmediately(workAgent.getNetworkCapabilities())) {
inOrder.verify(mMockNetd).networkCreate(
nativeNetworkConfigPhysical(workAgent.getNetwork().netId,
INetd.PERMISSION_SYSTEM));
@@ -18825,7 +18825,7 @@
}
private void verifyMtuSetOnWifiInterfaceOnlyUpToT(int mtu) throws Exception {
- if (!mService.shouldCreateNetworksImmediately()) {
+ if (!mService.shouldCreateNetworksImmediately(mWiFiAgent.getNetworkCapabilities())) {
verify(mMockNetd, times(1)).interfaceSetMtu(WIFI_IFNAME, mtu);
} else {
verify(mMockNetd, never()).interfaceSetMtu(eq(WIFI_IFNAME), anyInt());
@@ -18833,7 +18833,7 @@
}
private void verifyMtuSetOnWifiInterfaceOnlyStartingFromU(int mtu) throws Exception {
- if (mService.shouldCreateNetworksImmediately()) {
+ if (mService.shouldCreateNetworksImmediately(mWiFiAgent.getNetworkCapabilities())) {
verify(mMockNetd, times(1)).interfaceSetMtu(WIFI_IFNAME, mtu);
} else {
verify(mMockNetd, never()).interfaceSetMtu(eq(WIFI_IFNAME), anyInt());
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 e6e6ecc..087617a 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
@@ -99,6 +99,13 @@
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
@@ -183,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 {
@@ -247,6 +258,9 @@
doReturn(SERVICES_PRIORITY_LIST).`when`(resources).getStringArray(
R.array.config_nsdOffloadServicesPriority
)
+ doReturn(SERVICES_DENY_LIST).`when`(resources).getStringArray(
+ R.array.config_nsdOffloadServicesDenyList
+ )
ConnectivityResources.setResourcesContextForTest(context)
}
@@ -524,6 +538,44 @@
}
@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)
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 71a3274..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,45 +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.get(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS));
-
- // Verify the execute method
- final CompletableFuture<Boolean> future2 = new CompletableFuture<>();
- executor.execute(()-> future2.complete(true));
- testableLooper.processAllMessages();
- 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.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/connectivityservice/CSLocalNetworkProtectionTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalNetworkProtectionTest.kt
index 5bf6e04..84c9835 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSLocalNetworkProtectionTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalNetworkProtectionTest.kt
@@ -38,6 +38,7 @@
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
@@ -45,6 +46,7 @@
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)
@@ -78,6 +80,20 @@
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(
@@ -190,7 +206,7 @@
}
@Test
- fun testStackedLinkPropertiesWithDifferentLinkAddresses_AddressAddedInBpfMap() {
+ fun testAddingThenRemovingStackedLinkProperties_AddressAddedThenRemovedInBpfMap() {
val nr = nr(TRANSPORT_WIFI)
val cb = TestableNetworkCallback()
cm.requestNetwork(nr, cb)
@@ -230,49 +246,6 @@
)
// As both addresses are in stacked links, so no address should be removed from the map.
verify(bpfNetMaps, never()).removeLocalNetAccess(any(), any(), any(), any(), any())
- }
-
- @Test
- fun testRemovingStackedLinkProperties_AddressRemovedInBpfMap() {
- val nr = nr(TRANSPORT_WIFI)
- val cb = TestableNetworkCallback()
- cm.requestNetwork(nr, cb)
-
- val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
- val wifiLp2 = lp(WIFI_IFNAME_2, LOCAL_IPV4_LINK_ADDRRESS_1)
- // populating stacked link
- wifiLp.addStackedLink(wifiLp2)
- val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
- wifiAgent.connect()
- cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
-
- // Multicast and Broadcast address should always be populated in local_net_access map
- verifyPopulationOfMulticastAndBroadcastAddress()
- // Verifying IPv6 address should be populated in local_net_access map
- verify(bpfNetMaps).addLocalNetAccess(
- eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
- eq(WIFI_IFNAME),
- eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
- eq(0),
- eq(0),
- eq(false)
- )
-
- // Multicast and Broadcast address should always be populated on stacked link
- // in local_net_access map
- verifyPopulationOfMulticastAndBroadcastAddress(WIFI_IFNAME_2)
- // Verifying IPv4 matching prefix(10.0.0.0/8) should be populated as part of stacked link
- // in local_net_access map
- verify(bpfNetMaps).addLocalNetAccess(
- eq(PREFIX_LENGTH_IPV4 + 8),
- eq(WIFI_IFNAME_2),
- eq(InetAddresses.parseNumericAddress("10.0.0.0")),
- eq(0),
- eq(0),
- eq(false)
- )
- // As both addresses are in stacked links, so no address should be removed from the map.
- verify(bpfNetMaps, never()).removeLocalNetAccess(any(), any(), any(), any(), any())
// replacing link properties without stacked links
val wifiLp_3 = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
@@ -290,6 +263,107 @@
}
@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()
diff --git a/tests/unit/java/com/android/server/net/HeaderCompressionUtilsTest.kt b/tests/unit/java/com/android/server/net/HeaderCompressionUtilsTest.kt
index 8431194..7ebe384 100644
--- a/tests/unit/java/com/android/server/net/HeaderCompressionUtilsTest.kt
+++ b/tests/unit/java/com/android/server/net/HeaderCompressionUtilsTest.kt
@@ -17,12 +17,14 @@
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.android.internal.util.HexDump
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
@@ -184,6 +186,83 @@
}
@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"
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/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 7a5895f..0e8f824 100644
--- a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
+++ b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
@@ -40,6 +40,8 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assume.assumeTrue;
+
import static java.util.concurrent.TimeUnit.SECONDS;
import android.content.Context;
@@ -132,10 +134,6 @@
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 */);
}
@@ -217,6 +215,8 @@
@Test
public void otDaemonRestart_latestCountryCodeIsSetToOtDaemon() throws Exception {
+ assumeTrue(mOtCtl.isCountryCodeSupported());
+
runThreadCommand("force-country-code enabled CN");
runShellCommand("stop ot-daemon");
@@ -352,7 +352,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..dcccbf1 100644
--- a/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java
+++ b/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java
@@ -28,6 +28,7 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeTrue;
import android.content.Context;
import android.net.thread.utils.FullThreadDevice;
@@ -66,11 +67,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();
@@ -143,6 +140,8 @@
@Test
public void forceCountryCode_setCN_getCountryCodeReturnsCN() {
+ assumeTrue(mOtCtl.isCountryCodeSupported());
+
runThreadCommand("force-country-code enabled CN");
final String result = runThreadCommand("get-country-code");
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/integration/src/android/net/thread/utils/OtDaemonController.java b/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
index 9fbfa45..272685f 100644
--- a/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
+++ b/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
@@ -156,6 +156,12 @@
return executeCommandAndParse("extpanid").get(0);
}
+ public boolean isCountryCodeSupported() {
+ final String result = executeCommand("region");
+
+ return !result.equals("Error 12: NotImplemented\r\n");
+ }
+
public String executeCommand(String cmd) {
return SystemUtil.runShellCommand(OT_CTL + " " + cmd);
}
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,
- },
- },
}