Merge "try-catch when calling createContextAsUser" into main
diff --git a/OWNERS_core_networking b/OWNERS_core_networking
index 078ccde..4b73639 100644
--- a/OWNERS_core_networking
+++ b/OWNERS_core_networking
@@ -1,4 +1,5 @@
jchalard@google.com
+jimictw@google.com
junyulai@google.com
lorenzo@google.com
maze@google.com
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 1d2041b..8592af2 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -77,7 +77,7 @@
"name": "libnetworkstats_test"
},
{
- "name": "CtsTetheringTestLatestSdk",
+ "name": "CtsTetheringTest",
"options": [
{
"exclude-annotation": "com.android.testutils.NetworkStackModuleTest"
@@ -226,6 +226,9 @@
},
{
"name": "FrameworksNetIntegrationTests"
+ },
+ {
+ "name": "CtsTetheringTest"
}
],
"postsubmit": [
@@ -391,7 +394,7 @@
"name": "libnetworkstats_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
},
{
- "name": "CtsTetheringTestLatestSdk[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
+ "name": "CtsTetheringTest[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
"options": [
{
"exclude-annotation": "com.android.testutils.NetworkStackModuleTest"
@@ -406,7 +409,7 @@
"keywords": ["sim"]
},
{
- "name": "CtsTetheringTestLatestSdk[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
+ "name": "CtsTetheringTest[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
"keywords": ["sim"],
"options": [
{
diff --git a/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl b/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl
index 77e78bd..7d244e2 100644
--- a/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl
+++ b/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl
@@ -18,6 +18,7 @@
import android.net.IIntResultListener;
import android.net.ITetheringEventCallback;
import android.net.TetheringRequestParcel;
+import android.net.TetheringManager.TetheringRequest;
import android.os.ResultReceiver;
/** @hide */
@@ -37,6 +38,9 @@
void stopTethering(int type, String callerPkg, String callingAttributionTag,
IIntResultListener receiver);
+ void stopTetheringRequest(in TetheringRequest request, String callerPkg,
+ String callingAttributionTag, IIntResultListener receiver);
+
void requestLatestTetheringEntitlementResult(int type, in ResultReceiver receiver,
boolean showEntitlementUi, String callerPkg, String callingAttributionTag);
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index e81cbf0..f123dca 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -28,6 +28,7 @@
import android.content.Context;
import android.net.wifi.SoftApConfiguration;
import android.net.wifi.WifiManager;
+import android.os.Build;
import android.os.Bundle;
import android.os.ConditionVariable;
import android.os.IBinder;
@@ -657,6 +658,13 @@
}
}
+ private void unsupportedAfterV() {
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ throw new UnsupportedOperationException("Not supported after SDK version "
+ + Build.VERSION_CODES.VANILLA_ICE_CREAM);
+ }
+ }
+
/**
* Attempt to tether the named interface. This will setup a dhcp server
* on the interface, forward and NAT IP v4 packets and forward DNS requests
@@ -666,8 +674,10 @@
* access will of course fail until an upstream network interface becomes
* active.
*
- * @deprecated The only usages is PanService. It uses this for legacy reasons
- * and will migrate away as soon as possible.
+ * @deprecated Legacy tethering API. Callers should instead use
+ * {@link #startTethering(int, Executor, StartTetheringCallback)}.
+ * On SDK versions after {@link Build.VERSION_CODES.VANILLA_ICE_CREAM}, this will
+ * throw an UnsupportedOperationException.
*
* @param iface the interface name to tether.
* @return error a {@code TETHER_ERROR} value indicating success or failure type
@@ -677,6 +687,8 @@
@Deprecated
@SystemApi(client = MODULE_LIBRARIES)
public int tether(@NonNull final String iface) {
+ unsupportedAfterV();
+
final String callerPkg = mContext.getOpPackageName();
Log.i(TAG, "tether caller:" + callerPkg);
final RequestDispatcher dispatcher = new RequestDispatcher();
@@ -700,14 +712,18 @@
/**
* Stop tethering the named interface.
*
- * @deprecated The only usages is PanService. It uses this for legacy reasons
- * and will migrate away as soon as possible.
+ * @deprecated Legacy tethering API. Callers should instead use
+ * {@link #stopTethering(int)}.
+ * On SDK versions after {@link Build.VERSION_CODES.VANILLA_ICE_CREAM}, this will
+ * throw an UnsupportedOperationException.
*
* {@hide}
*/
@Deprecated
@SystemApi(client = MODULE_LIBRARIES)
public int untether(@NonNull final String iface) {
+ unsupportedAfterV();
+
final String callerPkg = mContext.getOpPackageName();
Log.i(TAG, "untether caller:" + callerPkg);
@@ -791,6 +807,46 @@
*/
@SuppressLint("UnflaggedApi")
public static final class TetheringRequest implements Parcelable {
+ /**
+ * Tethering started by an explicit call to startTethering.
+ * @hide
+ */
+ public static final int REQUEST_TYPE_EXPLICIT = 0;
+
+ /**
+ * Tethering implicitly started by broadcasts (LOHS and P2P). Can never be pending.
+ * @hide
+ */
+ public static final int REQUEST_TYPE_IMPLICIT = 1;
+
+ /**
+ * Tethering started by the legacy tether() call. Can only happen on V-.
+ * @hide
+ */
+ public static final int REQUEST_TYPE_LEGACY = 2;
+
+ /**
+ * Tethering started but there was no pending request found. This may happen if Tethering is
+ * started and immediately stopped before the link layer goes up, or if we get a link layer
+ * event without a prior call to startTethering (e.g. adb shell cmd wifi start-softap).
+ * @hide
+ */
+ public static final int REQUEST_TYPE_PLACEHOLDER = 3;
+
+ /**
+ * Type of request, used to keep track of whether the request was explicitly sent by
+ * startTethering, implicitly created by broadcasts, or via legacy tether().
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "TYPE_", value = {
+ REQUEST_TYPE_EXPLICIT,
+ REQUEST_TYPE_IMPLICIT,
+ REQUEST_TYPE_LEGACY,
+ REQUEST_TYPE_PLACEHOLDER,
+ })
+ public @interface RequestType {}
+
/** A configuration set for TetheringRequest. */
private final TetheringRequestParcel mRequestParcel;
@@ -850,6 +906,7 @@
mBuilderParcel.uid = Process.INVALID_UID;
mBuilderParcel.softApConfig = null;
mBuilderParcel.interfaceName = null;
+ mBuilderParcel.requestType = REQUEST_TYPE_EXPLICIT;
}
/**
@@ -1145,6 +1202,14 @@
}
/**
+ * Get the type of the request.
+ * @hide
+ */
+ public @RequestType int getRequestType() {
+ return mRequestParcel.requestType;
+ }
+
+ /**
* String of TetheringRequest detail.
* @hide
*/
@@ -1152,6 +1217,13 @@
public String toString() {
StringJoiner sj = new StringJoiner(", ", "TetheringRequest[ ", " ]");
sj.add(typeToString(mRequestParcel.tetheringType));
+ if (mRequestParcel.requestType == REQUEST_TYPE_IMPLICIT) {
+ sj.add("IMPLICIT");
+ } else if (mRequestParcel.requestType == REQUEST_TYPE_LEGACY) {
+ sj.add("LEGACY");
+ } else if (mRequestParcel.requestType == REQUEST_TYPE_PLACEHOLDER) {
+ sj.add("PLACEHOLDER");
+ }
if (mRequestParcel.localIPv4Address != null) {
sj.add("localIpv4Address=" + mRequestParcel.localIPv4Address);
}
@@ -1201,7 +1273,8 @@
public boolean equalsIgnoreUidPackage(TetheringRequest otherRequest) {
TetheringRequestParcel parcel = getParcel();
TetheringRequestParcel otherParcel = otherRequest.getParcel();
- return parcel.tetheringType == otherParcel.tetheringType
+ return parcel.requestType == otherParcel.requestType
+ && parcel.tetheringType == otherParcel.tetheringType
&& Objects.equals(parcel.localIPv4Address, otherParcel.localIPv4Address)
&& Objects.equals(parcel.staticClientAddress, otherParcel.staticClientAddress)
&& parcel.exemptFromEntitlementCheck == otherParcel.exemptFromEntitlementCheck
@@ -1344,7 +1417,25 @@
@FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
public void stopTethering(@NonNull TetheringRequest request,
@NonNull final Executor executor, @NonNull final StopTetheringCallback callback) {
- throw new UnsupportedOperationException();
+ Objects.requireNonNull(request);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+
+ final String callerPkg = mContext.getOpPackageName();
+ Log.i(TAG, "stopTethering: request=" + request + ", caller=" + callerPkg);
+ getConnector(c -> c.stopTetheringRequest(request, callerPkg, getAttributionTag(),
+ new IIntResultListener.Stub() {
+ @Override
+ public void onResult(final int resultCode) {
+ executor.execute(() -> {
+ if (resultCode == TETHER_ERROR_NO_ERROR) {
+ callback.onStopTetheringSucceeded();
+ } else {
+ callback.onStopTetheringFailed(resultCode);
+ }
+ });
+ }
+ }));
}
/**
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl b/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
index 97c9b9a..9863385 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
@@ -24,6 +24,7 @@
* @hide
*/
parcelable TetheringRequestParcel {
+ int requestType;
int tetheringType;
LinkAddress localIPv4Address;
LinkAddress staticClientAddress;
diff --git a/Tethering/proguard.flags b/Tethering/proguard.flags
index 47e2848..6d857b1 100644
--- a/Tethering/proguard.flags
+++ b/Tethering/proguard.flags
@@ -1,3 +1,6 @@
+# Keep JNI registered methods
+-keepclasseswithmembers,includedescriptorclasses class * { native <methods>; }
+
# Keep class's integer static field for MessageUtils to parsing their name.
-keepclassmembers class com.android.server.**,android.net.**,com.android.networkstack.** {
static final % POLICY_*;
@@ -7,18 +10,6 @@
static final % EVENT_*;
}
--keep class com.android.networkstack.tethering.util.BpfMap {
- native <methods>;
-}
-
--keep class com.android.networkstack.tethering.util.TcUtils {
- native <methods>;
-}
-
--keep class com.android.networkstack.tethering.util.TetheringUtils {
- native <methods>;
-}
-
# Ensure runtime-visible field annotations are kept when using R8 full mode.
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
-keep interface com.android.networkstack.tethering.util.Struct$Field {
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index fa6ce95..609d759 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -174,10 +174,10 @@
/**
* Request Tethering change.
*
- * @param request the TetheringRequest this IpServer was enabled with.
+ * @param tetheringType the downstream type of this IpServer.
* @param enabled enable or disable tethering.
*/
- public void requestEnableTethering(TetheringRequest request, boolean enabled) { }
+ public void requestEnableTethering(int tetheringType, boolean enabled) { }
}
/** Capture IpServer dependencies, for injection. */
@@ -429,9 +429,11 @@
return Collections.unmodifiableList(mDhcpLeases);
}
- /** Enable this IpServer. IpServer state machine will be tethered or localHotspot state. */
- public void enable(final int requestedState, final TetheringRequest request) {
- sendMessage(CMD_TETHER_REQUESTED, requestedState, 0, request);
+ /**
+ * Enable this IpServer. IpServer state machine will be tethered or localHotspot state based on
+ * the connectivity scope of the TetheringRequest. */
+ public void enable(@NonNull final TetheringRequest request) {
+ sendMessage(CMD_TETHER_REQUESTED, 0, 0, request);
}
/** Stop this IpServer. After this is called this IpServer should not be used any more. */
@@ -1026,11 +1028,11 @@
mLinkProperties.setInterfaceName(mIfaceName);
}
- private void maybeConfigureStaticIp(final TetheringRequest request) {
+ private void maybeConfigureStaticIp(@NonNull final TetheringRequest request) {
// Ignore static address configuration if they are invalid or null. In theory, static
// addresses should not be invalid here because TetheringManager do not allow caller to
// specify invalid static address configuration.
- if (request == null || request.getLocalIpv4Address() == null
+ if (request.getLocalIpv4Address() == null
|| request.getClientStaticIpv4Address() == null || !checkStaticAddressConfiguration(
request.getLocalIpv4Address(), request.getClientStaticIpv4Address())) {
return;
@@ -1053,13 +1055,13 @@
case CMD_TETHER_REQUESTED:
mLastError = TETHER_ERROR_NO_ERROR;
mTetheringRequest = (TetheringRequest) message.obj;
- switch (message.arg1) {
- case STATE_LOCAL_ONLY:
- maybeConfigureStaticIp((TetheringRequest) message.obj);
+ switch (mTetheringRequest.getConnectivityScope()) {
+ case CONNECTIVITY_SCOPE_LOCAL:
+ maybeConfigureStaticIp(mTetheringRequest);
transitionTo(mLocalHotspotState);
break;
- case STATE_TETHERED:
- maybeConfigureStaticIp((TetheringRequest) message.obj);
+ case CONNECTIVITY_SCOPE_GLOBAL:
+ maybeConfigureStaticIp(mTetheringRequest);
transitionTo(mTetheredState);
break;
default:
@@ -1189,8 +1191,8 @@
handleNewPrefixRequest((IpPrefix) message.obj);
break;
case CMD_NOTIFY_PREFIX_CONFLICT:
- mLog.i("restart tethering: " + mIfaceName);
- mCallback.requestEnableTethering(mTetheringRequest, false /* enabled */);
+ mLog.i("restart tethering: " + mInterfaceType);
+ mCallback.requestEnableTethering(mInterfaceType, false /* enabled */);
transitionTo(mWaitingForRestartState);
break;
case CMD_SERVICE_FAILED_TO_START:
@@ -1474,12 +1476,12 @@
case CMD_TETHER_UNREQUESTED:
transitionTo(mInitialState);
mLog.i("Untethered (unrequested) and restarting " + mIfaceName);
- mCallback.requestEnableTethering(mTetheringRequest, true /* enabled */);
+ mCallback.requestEnableTethering(mInterfaceType, true /* enabled */);
break;
case CMD_INTERFACE_DOWN:
transitionTo(mUnavailableState);
mLog.i("Untethered (interface down) and restarting " + mIfaceName);
- mCallback.requestEnableTethering(mTetheringRequest, true /* enabled */);
+ mCallback.requestEnableTethering(mInterfaceType, true /* enabled */);
break;
default:
return false;
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 21b420a..1249e85 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -28,6 +28,7 @@
import static android.net.ConnectivityManager.EXTRA_NETWORK_INFO;
import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
import static android.net.TetheringManager.ACTION_TETHER_STATE_CHANGED;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_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;
@@ -47,10 +48,12 @@
import static android.net.TetheringManager.TETHER_ERROR_SERVICE_UNAVAIL;
import static android.net.TetheringManager.TETHER_ERROR_UNAVAIL_IFACE;
import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_IFACE;
+import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_REQUEST;
import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_TYPE;
import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_FAILED;
import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STARTED;
import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STOPPED;
+import static android.net.TetheringManager.TetheringRequest.REQUEST_TYPE_PLACEHOLDER;
import static android.net.TetheringManager.toIfaces;
import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_INTERFACE_NAME;
import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_MODE;
@@ -70,6 +73,9 @@
import static com.android.networkstack.tethering.metrics.TetheringStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED;
import static com.android.networkstack.tethering.metrics.TetheringStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI;
import static com.android.networkstack.tethering.metrics.TetheringStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI_P2P;
+import static com.android.networkstack.tethering.metrics.TetheringStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI_P2P_SUCCESS;
+import static com.android.networkstack.tethering.metrics.TetheringStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI_SUCCESS;
+import static com.android.networkstack.tethering.metrics.TetheringStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_TETHER_WITH_PLACEHOLDER_REQUEST;
import static com.android.networkstack.tethering.util.TetheringMessageBase.BASE_MAIN_SM;
import android.app.usage.NetworkStatsManager;
@@ -108,6 +114,7 @@
import android.net.wifi.p2p.WifiP2pInfo;
import android.net.wifi.p2p.WifiP2pManager;
import android.os.Binder;
+import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
@@ -143,6 +150,7 @@
import com.android.net.module.util.NetdUtils;
import com.android.net.module.util.RoutingCoordinatorManager;
import com.android.net.module.util.SharedLog;
+import com.android.net.module.util.TerribleErrorLog;
import com.android.networkstack.apishim.common.BluetoothPanShim;
import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceCallbackShim;
import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceRequestShim;
@@ -236,7 +244,7 @@
// Currently active tethering requests per tethering type. Only one of each type can be
// requested at a time. After a tethering type is requested, the map keeps tethering parameters
// to be used after the interface comes up asynchronously.
- private final SparseArray<TetheringRequest> mActiveTetheringRequests =
+ private final SparseArray<TetheringRequest> mPendingTetheringRequests =
new SparseArray<>();
private final Context mContext;
@@ -600,15 +608,42 @@
// This method needs to exist because TETHERING_BLUETOOTH before Android T and TETHERING_WIGIG
// can't use enableIpServing.
private void processInterfaceStateChange(final String iface, boolean enabled) {
+ final int type = ifaceNameToType(iface);
// Do not listen to USB interface state changes or USB interface add/removes. USB tethering
// is driven only by USB_ACTION broadcasts.
- final int type = ifaceNameToType(iface);
if (type == TETHERING_USB || type == TETHERING_NCM) return;
+ // On T+, BLUETOOTH uses enableIpServing.
if (type == TETHERING_BLUETOOTH && SdkLevel.isAtLeastT()) return;
+ // Cannot happen: on S+, tetherableWigigRegexps is always empty.
+ if (type == TETHERING_WIGIG && SdkLevel.isAtLeastS()) return;
+
+ // After V, disallow this legacy codepath from starting tethering of any type:
+ // everything must call ensureIpServerStarted directly.
+ //
+ // Don't touch the teardown path for now. It's more complicated because:
+ // - ensureIpServerStarted and ensureIpServerStopped act on different
+ // tethering types.
+ // - Depending on the type, ensureIpServerStopped is either called twice (once
+ // on interface down and once on interface removed) or just once (on
+ // interface removed).
+ //
+ // Note that this only affects WIFI and WIFI_P2P. The other types are either
+ // ignored above, or ignored by ensureIpServerStarted. Note that even for WIFI
+ // and WIFI_P2P, this code should not ever run in normal use, because the
+ // hotspot and p2p code do not call tether(). It's possible that this could
+ // happen in the field due to unforeseen OEM modifications. If it does happen,
+ // a terrible error is logged in tether().
+ // TODO: fix the teardown path to stop depending on interface state notifications.
+ // These are not necessary since most/all link layers have their own teardown
+ // notifications, and can race with those notifications.
+ if (enabled && Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ return;
+ }
+
if (enabled) {
- ensureIpServerStarted(iface);
+ ensureIpServerStartedForInterface(iface);
} else {
ensureIpServerStopped(iface);
}
@@ -669,7 +704,7 @@
final IIntResultListener listener) {
mHandler.post(() -> {
final int type = request.getTetheringType();
- final TetheringRequest unfinishedRequest = mActiveTetheringRequests.get(type);
+ 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)) {
@@ -677,7 +712,7 @@
unfinishedRequest.getInterfaceName(), null);
mEntitlementMgr.stopProvisioningIfNeeded(type);
}
- mActiveTetheringRequests.put(type, request);
+ mPendingTetheringRequests.put(type, request);
if (request.isExemptFromEntitlementCheck()) {
mEntitlementMgr.setExemptedDownstreamType(type);
@@ -695,8 +730,42 @@
stopTetheringInternal(type);
});
}
+
+ private boolean isTetheringTypePendingOrServing(final int type) {
+ for (int i = 0; i < mPendingTetheringRequests.size(); i++) {
+ if (mPendingTetheringRequests.valueAt(i).getTetheringType() == type) return true;
+ }
+ for (TetherState state : mTetherStates.values()) {
+ // TODO: isCurrentlyServing only starts returning true once the IpServer has processed
+ // the CMD_TETHER_REQUESTED. Ensure that we consider the request to be serving even when
+ // that has not happened yet.
+ if (state.isCurrentlyServing() && state.ipServer.interfaceType() == type) return true;
+ }
+ return false;
+ }
+
+ void stopTetheringRequest(@NonNull final TetheringRequest request,
+ @NonNull final IIntResultListener listener) {
+ if (!isTetheringWithSoftApConfigEnabled()) return;
+ mHandler.post(() -> {
+ final int type = request.getTetheringType();
+ if (isTetheringTypePendingOrServing(type)) {
+ stopTetheringInternal(type);
+ try {
+ listener.onResult(TETHER_ERROR_NO_ERROR);
+ } catch (RemoteException ignored) { }
+ return;
+ }
+
+ // Request doesn't match any active requests, ignore.
+ try {
+ listener.onResult(TETHER_ERROR_UNKNOWN_REQUEST);
+ } catch (RemoteException ignored) { }
+ });
+ }
+
void stopTetheringInternal(int type) {
- mActiveTetheringRequests.remove(type);
+ mPendingTetheringRequests.remove(type);
enableTetheringInternal(type, false /* disabled */, null, null);
mEntitlementMgr.stopProvisioningIfNeeded(type);
@@ -750,7 +819,7 @@
// If changing tethering fail, remove corresponding request
// no matter who trigger the start/stop.
if (result != TETHER_ERROR_NO_ERROR) {
- mActiveTetheringRequests.remove(type);
+ mPendingTetheringRequests.remove(type);
mTetheringMetrics.updateErrorCode(type, result);
mTetheringMetrics.sendReport(type);
}
@@ -912,7 +981,9 @@
public void onAvailable(String iface) {
if (this != mBluetoothCallback) return;
- enableIpServing(TETHERING_BLUETOOTH, iface, getRequestedState(TETHERING_BLUETOOTH));
+ final TetheringRequest request =
+ getOrCreatePendingTetheringRequest(TETHERING_BLUETOOTH);
+ enableIpServing(request, iface);
mConfiguredBluetoothIface = iface;
}
@@ -967,7 +1038,9 @@
// Ethernet callback arrived after Ethernet tethering stopped. Ignore.
return;
}
- enableIpServing(TETHERING_ETHERNET, iface, getRequestedState(TETHERING_ETHERNET));
+
+ final TetheringRequest request = getOrCreatePendingTetheringRequest(TETHERING_ETHERNET);
+ enableIpServing(request, iface);
mConfiguredEthernetIface = iface;
}
@@ -988,10 +1061,8 @@
} else {
mConfiguredVirtualIface = iface;
}
- enableIpServing(
- TETHERING_VIRTUAL,
- mConfiguredVirtualIface,
- getRequestedState(TETHERING_VIRTUAL));
+ final TetheringRequest request = getOrCreatePendingTetheringRequest(TETHERING_VIRTUAL);
+ enableIpServing(request, mConfiguredVirtualIface);
} else if (mConfiguredVirtualIface != null) {
ensureIpServerStopped(mConfiguredVirtualIface);
mConfiguredVirtualIface = null;
@@ -999,32 +1070,121 @@
return TETHER_ERROR_NO_ERROR;
}
- void tether(String iface, int requestedState, final IIntResultListener listener) {
- mHandler.post(() -> {
- switch (ifaceNameToType(iface)) {
- case TETHERING_WIFI:
- TetheringStatsLog.write(
- CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
- CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI
- );
- break;
- case TETHERING_WIFI_P2P:
- TetheringStatsLog.write(
- CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
- CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI_P2P
- );
- break;
- default:
- // Do nothing
- break;
- }
- try {
- listener.onResult(tether(iface, requestedState));
- } catch (RemoteException e) { }
- });
+ /**
+ * 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;
}
- private int tether(String iface, int requestedState) {
+ /**
+ * 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;
+ }
+
+ /**
+ * 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");
+ TetheringRequest placeholder = new TetheringRequest.Builder(type).build();
+ placeholder.getParcel().requestType = REQUEST_TYPE_PLACEHOLDER;
+ return placeholder;
+ }
+
+ private void handleLegacyTether(String iface, final IIntResultListener listener) {
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ // After V, the TetheringManager and ConnectivityManager tether and untether methods
+ // throw UnsupportedOperationException, so this cannot happen in normal use. Ensure
+ // that this code cannot run even if callers use raw binder calls or other
+ // unsupported methods.
+ return;
+ }
+
+ final int type = ifaceNameToType(iface);
+ if (type == TETHERING_INVALID) {
+ Log.e(TAG, "Ignoring call to legacy tether for unknown iface " + iface);
+ try {
+ listener.onResult(TETHER_ERROR_UNKNOWN_IFACE);
+ } catch (RemoteException e) { }
+ }
+
+ final TetheringRequest request = createLegacyGlobalScopeTetheringRequest(type);
+ int result = tetherInternal(request, iface);
+ switch (type) {
+ case TETHERING_WIFI:
+ TerribleErrorLog.logTerribleError(TetheringStatsLog::write,
+ "Legacy tether API called on Wifi iface " + iface,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI);
+ if (result == TETHER_ERROR_NO_ERROR) {
+ TerribleErrorLog.logTerribleError(TetheringStatsLog::write,
+ "Legacy tether API succeeded on Wifi iface " + iface,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI_SUCCESS);
+ }
+ break;
+ case TETHERING_WIFI_P2P:
+ TerribleErrorLog.logTerribleError(TetheringStatsLog::write,
+ "Legacy tether API called on Wifi P2P iface " + iface,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI_P2P);
+ if (result == TETHER_ERROR_NO_ERROR) {
+ TerribleErrorLog.logTerribleError(TetheringStatsLog::write,
+ "Legacy tether API succeeded on Wifi P2P iface " + iface,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI_P2P_SUCCESS);
+ }
+ break;
+ default:
+ // Do nothing
+ break;
+ }
+ try {
+ listener.onResult(result);
+ } catch (RemoteException e) { }
+ }
+
+ /**
+ * Legacy tether API that starts tethering with CONNECTIVITY_SCOPE_GLOBAL on the given iface.
+ *
+ * This API relies on the IpServer having been started for the interface by
+ * processInterfaceStateChanged beforehand, which is only possible for
+ * - WIGIG Pre-S
+ * - BLUETOOTH Pre-T
+ * - WIFI
+ * - WIFI_P2P.
+ * Note that WIFI and WIFI_P2P already start tethering on their respective ifaces via
+ * WIFI_(AP/P2P_STATE_CHANGED broadcasts, which makes this API redundant for those types unless
+ * those broadcasts are disabled by OEM.
+ */
+ void legacyTether(String iface, final IIntResultListener listener) {
+ mHandler.post(() -> handleLegacyTether(iface, listener));
+ }
+
+ private int tetherInternal(@NonNull TetheringRequest request, String iface) {
if (DBG) Log.d(TAG, "Tethering " + iface);
TetherState tetherState = mTetherStates.get(iface);
if (tetherState == null) {
@@ -1037,29 +1197,38 @@
Log.e(TAG, "Tried to Tether an unavailable iface: " + iface + ", ignoring");
return TETHER_ERROR_UNAVAIL_IFACE;
}
- // NOTE: If a CMD_TETHER_REQUESTED message is already in the TISM's queue but not yet
+ // NOTE: If a CMD_TETHER_REQUESTED message is already in the IpServer's queue but not yet
// processed, this will be a no-op and it will not return an error.
//
// This code cannot race with untether() because they both run on the handler thread.
- final int type = tetherState.ipServer.interfaceType();
- final TetheringRequest request = mActiveTetheringRequests.get(type, null);
- if (request != null) {
- mActiveTetheringRequests.delete(type);
+ mPendingTetheringRequests.remove(request.getTetheringType());
+ tetherState.ipServer.enable(request);
+ if (request.getRequestType() == REQUEST_TYPE_PLACEHOLDER) {
+ TerribleErrorLog.logTerribleError(TetheringStatsLog::write,
+ "Started tethering with placeholder request: " + request,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_TETHER_WITH_PLACEHOLDER_REQUEST);
}
- tetherState.ipServer.enable(requestedState, request);
return TETHER_ERROR_NO_ERROR;
}
- void untether(String iface, final IIntResultListener listener) {
+ void legacyUntether(String iface, final IIntResultListener listener) {
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ // After V, the TetheringManager and ConnectivityManager tether and untether methods
+ // throw UnsupportedOperationException, so this cannot happen in normal use. Ensure
+ // that this code cannot run even if callers use raw binder calls or other
+ // unsupported methods.
+ return;
+ }
mHandler.post(() -> {
try {
- listener.onResult(untether(iface));
+ listener.onResult(legacyUntetherInternal(iface));
} catch (RemoteException e) {
}
});
}
- int untether(String iface) {
+ int legacyUntetherInternal(String iface) {
if (DBG) Log.d(TAG, "Untethering " + iface);
TetherState tetherState = mTetherStates.get(iface);
if (tetherState == null) {
@@ -1074,7 +1243,7 @@
return TETHER_ERROR_NO_ERROR;
}
- void untetherAll() {
+ void stopAllTethering() {
stopTethering(TETHERING_WIFI);
stopTethering(TETHERING_WIFI_P2P);
stopTethering(TETHERING_USB);
@@ -1098,22 +1267,6 @@
return mEntitlementMgr.isTetherProvisioningRequired(cfg);
}
- private int getRequestedState(int type) {
- final TetheringRequest request = mActiveTetheringRequests.get(type);
-
- // The request could have been deleted before we had a chance to complete it.
- // If so, assume that the scope is the default scope for this tethering type.
- // This likely doesn't matter - if the request has been deleted, then tethering is
- // likely going to be stopped soon anyway.
- final int connectivityScope = (request != null)
- ? request.getConnectivityScope()
- : TetheringRequest.getDefaultConnectivityScope(type);
-
- return connectivityScope == CONNECTIVITY_SCOPE_LOCAL
- ? IpServer.STATE_LOCAL_ONLY
- : IpServer.STATE_TETHERED;
- }
-
private int getServedUsbType(boolean forNcmFunction) {
// TETHERING_NCM is only used if the device does not use NCM for regular USB tethering.
if (forNcmFunction && !mConfig.isUsingNcm()) return TETHERING_NCM;
@@ -1244,7 +1397,7 @@
mLog.log("OBSERVED data saver changed");
handleDataSaverChanged();
} else if (action.equals(TetheringNotificationUpdater.ACTION_DISABLE_TETHERING)) {
- untetherAll();
+ stopAllTethering();
}
}
@@ -1408,14 +1561,14 @@
mDataSaverEnabled = isDataSaverEnabled;
if (mDataSaverEnabled) {
- untetherAll();
+ stopAllTethering();
}
}
}
@VisibleForTesting
- SparseArray<TetheringRequest> getActiveTetheringRequests() {
- return mActiveTetheringRequests;
+ SparseArray<TetheringRequest> getPendingTetheringRequests() {
+ return mPendingTetheringRequests;
}
@VisibleForTesting
@@ -1467,7 +1620,7 @@
mNotificationUpdater.notifyTetheringDisabledByRestriction();
// Untether from all downstreams since tethering is disallowed.
- mTethering.untetherAll();
+ mTethering.stopAllTethering();
}
return true;
@@ -1475,14 +1628,17 @@
}
}
- private void enableIpServing(int tetheringType, String ifname, int ipServingMode) {
- enableIpServing(tetheringType, ifname, ipServingMode, false /* isNcm */);
+ final TetheringRequest getPendingTetheringRequest(int type) {
+ return mPendingTetheringRequests.get(type, null);
}
- private void enableIpServing(int tetheringType, String ifname, int ipServingMode,
- boolean isNcm) {
- ensureIpServerStarted(ifname, tetheringType, isNcm);
- if (tether(ifname, ipServingMode) != TETHER_ERROR_NO_ERROR) {
+ private void enableIpServing(@NonNull TetheringRequest request, String ifname) {
+ enableIpServing(request, ifname, false /* isNcm */);
+ }
+
+ private void enableIpServing(@NonNull TetheringRequest request, String ifname, boolean isNcm) {
+ ensureIpServerStartedForType(ifname, request.getTetheringType(), isNcm);
+ if (tetherInternal(request, ifname) != TETHER_ERROR_NO_ERROR) {
Log.e(TAG, "unable start tethering on iface " + ifname);
}
}
@@ -1534,7 +1690,10 @@
mLog.e(ifname + " is not a tetherable iface, ignoring");
return;
}
- enableIpServing(type, ifname, IpServer.STATE_LOCAL_ONLY);
+ // No need to call getOrCreatePendingRequest. There can never be explicit requests for
+ // TETHERING_WIFI_P2P because enableTetheringInternal ignores that type.
+ final TetheringRequest request = createImplicitLocalOnlyTetheringRequest(type);
+ enableIpServing(request, ifname);
}
private void disableWifiP2pIpServingIfNeeded(String ifname) {
@@ -1544,17 +1703,54 @@
disableWifiIpServingCommon(TETHERING_WIFI_P2P, ifname);
}
+ // TODO: fold this in to enableWifiIpServing. We cannot do this at the moment because there
+ // are tests that send wifi AP broadcasts with a null interface. But if this can't happen on
+ // real devices, we should fix those tests to always pass in an interface.
+ private int maybeInferWifiTetheringType(String ifname) {
+ return SdkLevel.isAtLeastT() ? TETHERING_WIFI : ifaceNameToType(ifname);
+ }
+
private void enableWifiIpServing(String ifname, int wifiIpMode) {
mLog.log("request WiFi tethering - interface=" + ifname + " state=" + wifiIpMode);
// Map wifiIpMode values to IpServer.Callback serving states.
- final int ipServingMode;
+ TetheringRequest request;
+ final int type;
switch (wifiIpMode) {
case IFACE_IP_MODE_TETHERED:
- ipServingMode = IpServer.STATE_TETHERED;
+ type = maybeInferWifiTetheringType(ifname);
+ request = getOrCreatePendingTetheringRequest(type);
+ // Wifi requests will always have CONNECTIVITY_SCOPE_GLOBAL, because
+ // TetheringRequest.Builder will not allow callers to set CONNECTIVITY_SCOPE_LOCAL
+ // for TETHERING_WIFI. However, if maybeInferWifiTetheringType returns a non-Wifi
+ // type (which could happen on a pre-T implementation of Wi-Fi if the regexps are
+ // misconfigured), then force the connectivity scope to global in order to match the
+ // historical behavior.
+ request.getParcel().connectivityScope = CONNECTIVITY_SCOPE_GLOBAL;
break;
case IFACE_IP_MODE_LOCAL_ONLY:
- ipServingMode = IpServer.STATE_LOCAL_ONLY;
+ type = maybeInferWifiTetheringType(ifname);
+ // BUG: this request is incorrect - instead of LOHS, it will reflect whatever
+ // request (if any) is being processed for TETHERING_WIFI. However, this is the
+ // historical behaviour. It mostly works because a) most of the time there is no
+ // such request b) tetherinternal doesn't look at the connectivity scope of the
+ // request, it takes the scope from requestedState.
+ request = getPendingTetheringRequest(type);
+ if (request == null) {
+ request = createImplicitLocalOnlyTetheringRequest(TETHERING_WIFI);
+ } else {
+ // If we've taken this request from the pending requests, then force the
+ // connectivity scope to local so we start IpServer in local-only mode (this
+ // matches historical behavior). This should be OK since the connectivity scope
+ // is only used to start IpServer in the correct mode.
+ // TODO: This will break fuzzy-matching logic for start/stop tethering in the
+ // future. Figure out how to reconcile that with this forced scope.
+ // Possibly ignore the connectivity scope for wifi if both requests are
+ // explicit, since explicit Wifi requests may only have
+ // CONNECTIVITY_SCOPE_GLOBAL. Or possibly, don't add any edge case and
+ // treat it as a different request entirely.
+ request.getParcel().connectivityScope = CONNECTIVITY_SCOPE_LOCAL;
+ }
break;
default:
mLog.e("Cannot enable IP serving in unknown WiFi mode: " + wifiIpMode);
@@ -1563,14 +1759,13 @@
// After T, tethering always trust the iface pass by state change intent. This allow
// tethering to deprecate tetherable wifi regexs after T.
- final int type = SdkLevel.isAtLeastT() ? TETHERING_WIFI : ifaceNameToType(ifname);
if (!checkTetherableType(type)) {
mLog.e(ifname + " is not a tetherable iface, ignoring");
return;
}
if (!TextUtils.isEmpty(ifname)) {
- enableIpServing(type, ifname, ipServingMode);
+ enableIpServing(request, ifname);
} else {
mLog.e("Cannot enable IP serving on missing interface name");
}
@@ -1591,7 +1786,6 @@
// for both TETHERING_USB and TETHERING_NCM, so the local-only NCM interface will be
// stopped immediately.
final int tetheringType = getServedUsbType(forNcmFunction);
- final int requestedState = getRequestedState(tetheringType);
String[] ifaces = null;
try {
ifaces = mNetd.interfaceGetList();
@@ -1600,10 +1794,11 @@
return;
}
+ final TetheringRequest request = getOrCreatePendingTetheringRequest(tetheringType);
if (ifaces != null) {
for (String iface : ifaces) {
if (ifaceNameToType(iface) == tetheringType) {
- enableIpServing(tetheringType, iface, requestedState, forNcmFunction);
+ enableIpServing(request, iface, forNcmFunction);
return;
}
}
@@ -2232,9 +2427,9 @@
break;
}
case EVENT_REQUEST_CHANGE_DOWNSTREAM: {
- final boolean enabled = message.arg1 == 1;
- final TetheringRequest request = (TetheringRequest) message.obj;
- enableTetheringInternal(request.getTetheringType(), enabled, null, null);
+ final int tetheringType = message.arg1;
+ final Boolean enabled = (Boolean) message.obj;
+ enableTetheringInternal(tetheringType, enabled, null, null);
break;
}
default:
@@ -2812,9 +3007,9 @@
}
@Override
- public void requestEnableTethering(TetheringRequest request, boolean enabled) {
+ public void requestEnableTethering(int tetheringType, boolean enabled) {
mTetherMainSM.sendMessage(TetherMainSM.EVENT_REQUEST_CHANGE_DOWNSTREAM,
- enabled ? 1 : 0, 0, request);
+ tetheringType, 0, enabled ? Boolean.TRUE : Boolean.FALSE);
}
}
@@ -2835,7 +3030,7 @@
return type != TETHERING_INVALID;
}
- private void ensureIpServerStarted(final String iface) {
+ private void ensureIpServerStartedForInterface(final String iface) {
// If we don't care about this type of interface, ignore.
final int interfaceType = ifaceNameToType(iface);
if (!checkTetherableType(interfaceType)) {
@@ -2844,10 +3039,11 @@
return;
}
- ensureIpServerStarted(iface, interfaceType, false /* isNcm */);
+ ensureIpServerStartedForType(iface, interfaceType, false /* isNcm */);
}
- private void ensureIpServerStarted(final String iface, int interfaceType, boolean isNcm) {
+ private void ensureIpServerStartedForType(final String iface, int interfaceType,
+ boolean isNcm) {
// If we have already started a TISM for this interface, skip.
if (mTetherStates.containsKey(iface)) {
mLog.log("active iface (" + iface + ") reported as added, ignoring");
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringService.java b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
index 0c44a38..b553f46 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringService.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
@@ -111,7 +111,7 @@
IIntResultListener listener) {
if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, listener)) return;
- mTethering.tether(iface, IpServer.STATE_TETHERED, listener);
+ mTethering.legacyTether(iface, listener);
}
@Override
@@ -119,7 +119,7 @@
IIntResultListener listener) {
if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, listener)) return;
- mTethering.untether(iface, listener);
+ mTethering.legacyUntether(iface, listener);
}
@Override
@@ -159,6 +159,18 @@
}
@Override
+ public void stopTetheringRequest(TetheringRequest request,
+ String callerPkg, String callingAttributionTag,
+ IIntResultListener listener) {
+ if (request == null) return;
+ if (listener == null) return;
+ if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, listener)) return;
+ request.setUid(getBinderCallingUid());
+ request.setPackageName(callerPkg);
+ mTethering.stopTetheringRequest(request, listener);
+ }
+
+ @Override
public void requestLatestTetheringEntitlementResult(int type, ResultReceiver receiver,
boolean showEntitlementUi, String callerPkg, String callingAttributionTag) {
if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, receiver)) return;
@@ -200,7 +212,7 @@
if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, listener)) return;
try {
- mTethering.untetherAll();
+ mTethering.stopAllTethering();
listener.onResult(TETHER_ERROR_NO_ERROR);
} catch (RemoteException e) { }
}
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index 680e81d..84b301f 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -48,7 +48,6 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.clearInvocations;
@@ -74,6 +73,7 @@
import android.net.LinkProperties;
import android.net.MacAddress;
import android.net.RouteInfo;
+import android.net.TetheringManager.TetheringRequest;
import android.net.dhcp.DhcpServerCallbacks;
import android.net.dhcp.DhcpServingParamsParcel;
import android.net.dhcp.IDhcpEventCallbacks;
@@ -240,7 +240,8 @@
Set<LinkAddress> upstreamAddresses, boolean usingLegacyDhcp, boolean usingBpfOffload)
throws Exception {
initStateMachine(interfaceType, usingLegacyDhcp, usingBpfOffload);
- dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+ createMockTetheringRequest(CONNECTIVITY_SCOPE_GLOBAL));
verify(mBpfCoordinator).addIpServer(mIpServer);
if (upstreamIface != null) {
InterfaceParams interfaceParams = mDependencies.getInterfaceParams(upstreamIface);
@@ -345,7 +346,8 @@
public void canBeTetheredAsBluetooth() throws Exception {
initStateMachine(TETHERING_BLUETOOTH);
- dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+ createMockTetheringRequest(CONNECTIVITY_SCOPE_GLOBAL));
InOrder inOrder = inOrder(mCallback, mNetd, mRoutingCoordinatorManager);
if (isAtLeastT()) {
inOrder.verify(mRoutingCoordinatorManager)
@@ -400,7 +402,8 @@
public void canBeTetheredAsUsb() throws Exception {
initStateMachine(TETHERING_USB);
- dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+ createMockTetheringRequest(CONNECTIVITY_SCOPE_GLOBAL));
InOrder inOrder = inOrder(mCallback, mNetd, mRoutingCoordinatorManager);
inOrder.verify(mRoutingCoordinatorManager).requestStickyDownstreamAddress(anyInt(),
eq(CONNECTIVITY_SCOPE_GLOBAL), any());
@@ -423,7 +426,8 @@
public void canBeTetheredAsWifiP2p_NotUsingDedicatedIp() throws Exception {
initStateMachine(TETHERING_WIFI_P2P);
- dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_LOCAL_ONLY);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+ createMockTetheringRequest(CONNECTIVITY_SCOPE_LOCAL));
InOrder inOrder = inOrder(mCallback, mNetd, mRoutingCoordinatorManager);
inOrder.verify(mRoutingCoordinatorManager).requestStickyDownstreamAddress(anyInt(),
eq(CONNECTIVITY_SCOPE_LOCAL), any());
@@ -447,7 +451,8 @@
initStateMachine(TETHERING_WIFI_P2P, false /* usingLegacyDhcp */, DEFAULT_USING_BPF_OFFLOAD,
true /* shouldEnableWifiP2pDedicatedIp */);
- dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_LOCAL_ONLY);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+ createMockTetheringRequest(CONNECTIVITY_SCOPE_LOCAL));
InOrder inOrder = inOrder(mCallback, mNetd, mRoutingCoordinatorManager);
// When using WiFi P2p dedicated IP, the IpServer just picks the IP address without
// requesting for it at RoutingCoordinatorManager.
@@ -627,7 +632,8 @@
initStateMachine(TETHERING_USB);
doThrow(RemoteException.class).when(mNetd).tetherInterfaceAdd(IFACE_NAME);
- dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+ createMockTetheringRequest(CONNECTIVITY_SCOPE_GLOBAL));
InOrder usbTeardownOrder = inOrder(mNetd, mCallback);
usbTeardownOrder.verify(mNetd).interfaceSetCfg(
argThat(cfg -> IFACE_NAME.equals(cfg.ifName)));
@@ -713,7 +719,8 @@
@Test
public void startsDhcpServerOnNcm() throws Exception {
initStateMachine(TETHERING_NCM);
- dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_LOCAL_ONLY);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+ createMockTetheringRequest(CONNECTIVITY_SCOPE_LOCAL));
dispatchTetherConnectionChanged(UPSTREAM_IFACE);
assertDhcpStarted(new IpPrefix("192.168.42.0/24"));
@@ -722,7 +729,8 @@
@Test
public void testOnNewPrefixRequest() throws Exception {
initStateMachine(TETHERING_NCM);
- dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_LOCAL_ONLY);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+ createMockTetheringRequest(CONNECTIVITY_SCOPE_LOCAL));
final IDhcpEventCallbacks eventCallbacks;
final ArgumentCaptor<IDhcpEventCallbacks> dhcpEventCbsCaptor =
@@ -911,7 +919,8 @@
doNothing().when(mDependencies).makeDhcpServer(any(), mDhcpParamsCaptor.capture(),
cbCaptor.capture());
initStateMachine(TETHERING_WIFI);
- dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+ createMockTetheringRequest(CONNECTIVITY_SCOPE_GLOBAL));
verify(mDhcpServer, never()).startWithCallbacks(any(), any());
// No stop dhcp server because dhcp server is not created yet.
@@ -957,14 +966,22 @@
assertDhcpServingParams(mDhcpParamsCaptor.getValue(), expectedPrefix);
}
+ private TetheringRequest createMockTetheringRequest(int connectivityScope) {
+ TetheringRequest request = mock(TetheringRequest.class);
+ when(request.getConnectivityScope()).thenReturn(connectivityScope);
+ return request;
+ }
+
/**
* Send a command to the state machine under test, and run the event loop to idle.
*
* @param command One of the IpServer.CMD_* constants.
- * @param arg1 An additional argument to pass.
+ * @param arg1 An additional argument to pass.
+ * @param arg2 An additional argument to pass.
+ * @param obj An additional object to pass.
*/
- private void dispatchCommand(int command, int arg1) {
- mIpServer.sendMessage(command, arg1);
+ private void dispatchCommand(int command, int arg1, int arg2, Object obj) {
+ mIpServer.sendMessage(command, arg1, arg2, obj);
mLooper.dispatchAll();
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
index c329142..f9e3a6a 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
@@ -186,9 +186,11 @@
// - Test bluetooth prefix is reserved.
when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(
getSubAddress(mBluetoothAddress.getAddress().getAddress()));
- final LinkAddress hotspotAddress = requestDownstreamAddress(mHotspotIpServer);
+ final LinkAddress hotspotAddress = requestStickyDownstreamAddress(mHotspotIpServer,
+ CONNECTIVITY_SCOPE_GLOBAL);
final IpPrefix hotspotPrefix = asIpPrefix(hotspotAddress);
assertNotEquals(asIpPrefix(mBluetoothAddress), hotspotPrefix);
+ releaseDownstream(mHotspotIpServer);
// - Test previous enabled hotspot prefix(cached prefix) is reserved.
when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(
@@ -207,7 +209,6 @@
assertNotEquals(asIpPrefix(mLegacyWifiP2pAddress), etherPrefix);
assertNotEquals(asIpPrefix(mBluetoothAddress), etherPrefix);
assertNotEquals(hotspotPrefix, etherPrefix);
- releaseDownstream(mHotspotIpServer);
releaseDownstream(mEthernetIpServer);
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
index cc80251..b550ada 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
@@ -53,7 +53,6 @@
import android.net.TetheringManager;
import android.net.TetheringManager.TetheringRequest;
import android.net.TetheringRequestParcel;
-import android.net.ip.IpServer;
import android.os.Bundle;
import android.os.ConditionVariable;
import android.os.Handler;
@@ -219,7 +218,7 @@
mTetheringConnector.tether(TEST_IFACE_NAME, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG, result);
verify(mTethering).isTetheringSupported();
verify(mTethering).isTetheringAllowed();
- verify(mTethering).tether(TEST_IFACE_NAME, IpServer.STATE_TETHERED, result);
+ verify(mTethering).legacyTether(TEST_IFACE_NAME, result);
}
@Test
@@ -267,7 +266,7 @@
result);
verify(mTethering).isTetheringSupported();
verify(mTethering).isTetheringAllowed();
- verify(mTethering).untether(eq(TEST_IFACE_NAME), eq(result));
+ verify(mTethering).legacyUntether(eq(TEST_IFACE_NAME), eq(result));
}
@Test
@@ -661,7 +660,7 @@
mTetheringConnector.stopAllTethering(TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG, result);
verify(mTethering).isTetheringSupported();
verify(mTethering).isTetheringAllowed();
- verify(mTethering).untetherAll();
+ verify(mTethering).stopAllTethering();
result.assertResult(TETHER_ERROR_NO_ERROR);
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
index 97758cf..f7a44f1 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -933,11 +933,18 @@
// it creates a IpServer and sends out a broadcast indicating that the
// interface is "available".
if (emulateInterfaceStatusChanged) {
- // There is 1 IpServer state change event: STATE_AVAILABLE
- verify(mNotificationUpdater, times(1)).onDownstreamChanged(DOWNSTREAM_NONE);
- verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
- verify(mWifiManager).updateInterfaceIpState(
- TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ if (!SdkLevel.isAtLeastB()) {
+ // There is 1 IpServer state change event: STATE_AVAILABLE
+ verify(mNotificationUpdater, times(1)).onDownstreamChanged(DOWNSTREAM_NONE);
+ verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
+ verify(mWifiManager).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ } else {
+ // Starting in B, ignore the interfaceStatusChanged
+ verify(mNotificationUpdater, never()).onDownstreamChanged(DOWNSTREAM_NONE);
+ verify(mWifiManager, never()).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ }
}
verifyNoMoreInteractions(mNetd);
verifyNoMoreInteractions(mWifiManager);
@@ -957,8 +964,8 @@
mTethering.startTethering(request, TEST_CALLER_PKG, null);
mLooper.dispatchAll();
- assertEquals(1, mTethering.getActiveTetheringRequests().size());
- assertEquals(request, mTethering.getActiveTetheringRequests().get(TETHERING_USB));
+ assertEquals(1, mTethering.getPendingTetheringRequests().size());
+ assertEquals(request, mTethering.getPendingTetheringRequests().get(TETHERING_USB));
if (mTethering.getTetheringConfiguration().isUsingNcm()) {
verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_NCM);
@@ -1934,7 +1941,6 @@
workingLocalOnlyHotspotEnrichedApBroadcast(false);
}
- // TODO: Test with and without interfaceStatusChanged().
@Test
public void failingWifiTetheringLegacyApBroadcast() throws Exception {
initTetheringOnTestThread();
@@ -1953,12 +1959,20 @@
// tethering mode is to be started.
mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
sendWifiApStateChanged(WIFI_AP_STATE_ENABLED);
+ mLooper.dispatchAll();
- // There is 1 IpServer state change event: STATE_AVAILABLE
- verify(mNotificationUpdater, times(1)).onDownstreamChanged(DOWNSTREAM_NONE);
- verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
- verify(mWifiManager).updateInterfaceIpState(
- TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ if (!SdkLevel.isAtLeastB()) {
+ // There is 1 IpServer state change event: STATE_AVAILABLE from interfaceStatusChanged
+ verify(mNotificationUpdater, times(1)).onDownstreamChanged(DOWNSTREAM_NONE);
+ verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
+ verify(mWifiManager).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ } else {
+ // Starting in B, ignore the interfaceStatusChanged
+ verify(mNotificationUpdater, never()).onDownstreamChanged(DOWNSTREAM_NONE);
+ verify(mWifiManager, never()).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ }
verifyNoMoreInteractions(mNetd);
verifyNoMoreInteractions(mWifiManager);
}
@@ -2098,7 +2112,7 @@
verify(mNotificationUpdater, times(expectedInteractionsWithShowNotification))
.notifyTetheringDisabledByRestriction();
- verify(mockTethering, times(expectedInteractionsWithShowNotification)).untetherAll();
+ verify(mockTethering, times(expectedInteractionsWithShowNotification)).stopAllTethering();
}
@Test
@@ -2170,7 +2184,7 @@
runUsbTethering(upstreamState);
assertContains(Arrays.asList(mTethering.getTetheredIfaces()), TEST_RNDIS_IFNAME);
assertTrue(mTethering.isTetheringActive());
- assertEquals(0, mTethering.getActiveTetheringRequests().size());
+ assertEquals(0, mTethering.getPendingTetheringRequests().size());
final Tethering.UserRestrictionActionListener ural = makeUserRestrictionActionListener(
mTethering, false /* currentDisallow */, true /* nextDisallow */);
@@ -2361,14 +2375,19 @@
UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
initTetheringUpstream(upstreamState);
when(mWifiManager.startTetheredHotspot(null)).thenReturn(true);
- mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
- mLooper.dispatchAll();
- tetherState = callback.pollTetherStatesChanged();
- assertArrayEquals(tetherState.availableList, new TetheringInterface[] {wifiIface});
mTethering.startTethering(createTetheringRequest(TETHERING_WIFI), TEST_CALLER_PKG,
null);
+ mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
+ mLooper.dispatchAll();
+ if (SdkLevel.isAtLeastB()) {
+ // Starting in B, ignore the interfaceStatusChanged
+ callback.assertNoStateChangeCallback();
+ }
sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+ mLooper.dispatchAll();
+ tetherState = callback.pollTetherStatesChanged();
+ assertArrayEquals(tetherState.availableList, new TetheringInterface[] {wifiIface});
tetherState = callback.pollTetherStatesChanged();
assertArrayEquals(tetherState.tetheredList, new TetheringInterface[] {wifiIface});
callback.expectUpstreamChanged(upstreamState.network);
@@ -2459,19 +2478,25 @@
UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
initTetheringUpstream(upstreamState);
when(mWifiManager.startTetheredHotspot(null)).thenReturn(true);
+
+ // Enable wifi tethering
+ mBinderCallingUid = TEST_CALLER_UID;
+ mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, null);
mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
mLooper.dispatchAll();
+ if (SdkLevel.isAtLeastB()) {
+ // Starting in B, ignore the interfaceStatusChanged
+ callback.assertNoStateChangeCallback();
+ }
+ sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+ mLooper.dispatchAll();
+ // Verify we see Available -> Tethered states
assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig},
callback.pollTetherStatesChanged().availableList);
assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig},
differentCallback.pollTetherStatesChanged().availableList);
assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig},
settingsCallback.pollTetherStatesChanged().availableList);
-
- // Enable wifi tethering
- mBinderCallingUid = TEST_CALLER_UID;
- mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, null);
- sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
assertArrayEquals(new TetheringInterface[] {wifiIfaceWithConfig},
callback.pollTetherStatesChanged().tetheredList);
assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig},
@@ -3426,7 +3451,7 @@
mTethering.interfaceStatusChanged(TEST_BT_IFNAME, false);
mTethering.interfaceStatusChanged(TEST_BT_IFNAME, true);
final ResultListener tetherResult = new ResultListener(TETHER_ERROR_NO_ERROR);
- mTethering.tether(TEST_BT_IFNAME, IpServer.STATE_TETHERED, tetherResult);
+ mTethering.legacyTether(TEST_BT_IFNAME, tetherResult);
mLooper.dispatchAll();
tetherResult.assertHasResult();
@@ -3446,7 +3471,7 @@
mTethering.stopTethering(TETHERING_BLUETOOTH);
mLooper.dispatchAll();
final ResultListener untetherResult = new ResultListener(TETHER_ERROR_NO_ERROR);
- mTethering.untether(TEST_BT_IFNAME, untetherResult);
+ mTethering.legacyUntether(TEST_BT_IFNAME, untetherResult);
mLooper.dispatchAll();
untetherResult.assertHasResult();
verifySetBluetoothTethering(false /* enable */, false /* bindToPanService */);
@@ -3476,7 +3501,7 @@
mTethering.interfaceStatusChanged(TEST_BT_IFNAME, false);
mTethering.interfaceStatusChanged(TEST_BT_IFNAME, true);
final ResultListener tetherResult = new ResultListener(TETHER_ERROR_NO_ERROR);
- mTethering.tether(TEST_BT_IFNAME, IpServer.STATE_TETHERED, tetherResult);
+ mTethering.legacyTether(TEST_BT_IFNAME, tetherResult);
mLooper.dispatchAll();
tetherResult.assertHasResult();
}
diff --git a/bpf/headers/include/bpf/BpfClassic.h b/bpf/headers/include/bpf/BpfClassic.h
index e6cef89..26d8ad5 100644
--- a/bpf/headers/include/bpf/BpfClassic.h
+++ b/bpf/headers/include/bpf/BpfClassic.h
@@ -170,6 +170,9 @@
// IPv6 extension headers (HOPOPTS, DSTOPS, FRAG) begin with a u8 nexthdr
#define BPF_LOAD_NETX_RELATIVE_V6EXTHDR_NEXTHDR BPF_LOAD_NETX_RELATIVE_L4_U8(0)
+// IPv6 MLD start with u8 type
+#define BPF_LOAD_NETX_RELATIVE_MLD_TYPE BPF_LOAD_NETX_RELATIVE_L4_U8(0)
+
// IPv6 fragment header is always exactly 8 bytes long
#define BPF_LOAD_CONSTANT_V6FRAGHDR_LEN \
BPF_STMT(BPF_LD | BPF_IMM, 8)
diff --git a/bpf/headers/include/bpf/BpfMap.h b/bpf/headers/include/bpf/BpfMap.h
index 1037beb..576cca6 100644
--- a/bpf/headers/include/bpf/BpfMap.h
+++ b/bpf/headers/include/bpf/BpfMap.h
@@ -26,6 +26,7 @@
#include "BpfSyscallWrappers.h"
#include "bpf/BpfUtils.h"
+#include <cstdio>
#include <functional>
namespace android {
@@ -35,6 +36,30 @@
using base::unique_fd;
using std::function;
+#ifdef BPF_MAP_MAKE_VISIBLE_FOR_TESTING
+#undef BPFMAP_VERBOSE_ABORT
+#define BPFMAP_VERBOSE_ABORT
+#endif
+
+[[noreturn]] __attribute__((__format__(__printf__, 2, 3))) static inline
+void Abort(int __unused error, const char* __unused fmt, ...) {
+#ifdef BPFMAP_VERBOSE_ABORT
+ va_list va;
+ va_start(va, fmt);
+
+ fflush(stdout);
+ vfprintf(stderr, fmt, va);
+ if (error) fprintf(stderr, "; errno=%d [%s]", error, strerror(error));
+ putc('\n', stderr);
+ fflush(stderr);
+
+ va_end(va);
+#endif
+
+ abort();
+}
+
+
// This is a class wrapper for eBPF maps. The eBPF map is a special in-kernel
// data structure that stores data in <Key, Value> pairs. It can be read/write
// from userspace by passing syscalls with the map file descriptor. This class
@@ -60,14 +85,21 @@
protected:
void abortOnMismatch(bool writable) const {
- if (!mMapFd.ok()) abort();
+ if (!mMapFd.ok()) Abort(errno, "mMapFd %d is not valid", mMapFd.get());
if (isAtLeastKernelVersion(4, 14, 0)) {
int flags = bpfGetFdMapFlags(mMapFd);
- if (flags < 0) abort();
- if (flags & BPF_F_WRONLY) abort();
- if (writable && (flags & BPF_F_RDONLY)) abort();
- if (bpfGetFdKeySize(mMapFd) != sizeof(Key)) abort();
- if (bpfGetFdValueSize(mMapFd) != sizeof(Value)) abort();
+ if (flags < 0) Abort(errno, "bpfGetFdMapFlags fail: flags=%d", flags);
+ if (flags & BPF_F_WRONLY) Abort(0, "map is write-only (flags=0x%X)", flags);
+ if (writable && (flags & BPF_F_RDONLY))
+ Abort(0, "writable map is actually read-only (flags=0x%X)", flags);
+ int keySize = bpfGetFdKeySize(mMapFd);
+ if (keySize != sizeof(Key))
+ Abort(errno, "map key size mismatch (expected=%zu, actual=%d)",
+ sizeof(Key), keySize);
+ int valueSize = bpfGetFdValueSize(mMapFd);
+ if (valueSize != sizeof(Value))
+ Abort(errno, "map value size mismatch (expected=%zu, actual=%d)",
+ sizeof(Value), valueSize);
}
}
@@ -278,8 +310,8 @@
[[clang::reinitializes]] Result<void> resetMap(bpf_map_type map_type,
uint32_t max_entries,
uint32_t map_flags = 0) {
- if (map_flags & BPF_F_WRONLY) abort();
- if (map_flags & BPF_F_RDONLY) abort();
+ if (map_flags & BPF_F_WRONLY) Abort(0, "map_flags is write-only");
+ if (map_flags & BPF_F_RDONLY) Abort(0, "map_flags is read-only");
mMapFd.reset(createMap(map_type, sizeof(Key), sizeof(Value), max_entries,
map_flags));
if (!mMapFd.ok()) return ErrnoErrorf("BpfMap::resetMap() failed");
diff --git a/bpf/headers/include/bpf/BpfUtils.h b/bpf/headers/include/bpf/BpfUtils.h
index 9dd5822..9e8b2c7 100644
--- a/bpf/headers/include/bpf/BpfUtils.h
+++ b/bpf/headers/include/bpf/BpfUtils.h
@@ -63,9 +63,9 @@
// 4.9 kernels. The kernel code of socket release on pf_key socket will
// explicitly call synchronize_rcu() which is exactly what we need.
//
- // Linux 4.14/4.19/5.4/5.10/5.15/6.1 (and 6.3-rc5) still have this same behaviour.
+ // Linux 4.14/4.19/5.4/5.10/5.15/6.1/6.6/6.12 (& 6.13) have this behaviour.
// see net/key/af_key.c: pfkey_release() -> synchronize_rcu()
- // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/key/af_key.c?h=v6.3-rc5#n185
+ // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/key/af_key.c?h=v6.13#n185
const int pfSocket = socket(AF_KEY, SOCK_RAW | SOCK_CLOEXEC, PF_KEY_V2);
if (pfSocket < 0) {
diff --git a/bpf/headers/include/bpf_helpers.h b/bpf/headers/include/bpf_helpers.h
index b994a9f..6a0e5a8 100644
--- a/bpf/headers/include/bpf_helpers.h
+++ b/bpf/headers/include/bpf_helpers.h
@@ -62,8 +62,8 @@
// Android Mainline BpfLoader when running on Android V (sdk=35)
#define BPFLOADER_MAINLINE_V_VERSION (BPFLOADER_MAINLINE_U_QPR3_VERSION + 1u)
-// Android Mainline BpfLoader when running on Android W (sdk=36)
-#define BPFLOADER_MAINLINE_W_VERSION (BPFLOADER_MAINLINE_V_VERSION + 1u)
+// Android Mainline BpfLoader when running on Android 25Q2 (sdk=36)
+#define BPFLOADER_MAINLINE_25Q2_VERSION (BPFLOADER_MAINLINE_V_VERSION + 1u)
/* For mainline module use, you can #define BPFLOADER_{MIN/MAX}_VER
* before #include "bpf_helpers.h" to change which bpfloaders will
@@ -122,28 +122,46 @@
*/
#define CRITICAL(REASON) char _critical[] SECTION("critical") = (REASON)
-/*
- * Helper functions called from eBPF programs written in C. These are
- * implemented in the kernel sources.
- */
+// Helpers for writing kernel version specific bpf programs
struct kver_uint { unsigned int kver; };
#define KVER_(v) ((struct kver_uint){ .kver = (v) })
#define KVER(a, b, c) KVER_(((a) << 24) + ((b) << 16) + (c))
#define KVER_NONE KVER_(0)
+#define KVER_4_9 KVER(4, 9, 0)
#define KVER_4_14 KVER(4, 14, 0)
#define KVER_4_19 KVER(4, 19, 0)
#define KVER_5_4 KVER(5, 4, 0)
-#define KVER_5_8 KVER(5, 8, 0)
-#define KVER_5_9 KVER(5, 9, 0)
#define KVER_5_10 KVER(5, 10, 0)
#define KVER_5_15 KVER(5, 15, 0)
#define KVER_6_1 KVER(6, 1, 0)
#define KVER_6_6 KVER(6, 6, 0)
+#define KVER_6_12 KVER(6, 12, 0)
#define KVER_INF KVER_(0xFFFFFFFFu)
#define KVER_IS_AT_LEAST(kver, a, b, c) ((kver).kver >= KVER(a, b, c).kver)
+// Helpers for writing sdk level specific bpf programs
+//
+// Note: we choose to follow sdk api level values, but there is no real need for this:
+// These just need to be monotonically increasing. We could also use values ten or even
+// a hundred times larger to leave room for quarters or months. We may also just use
+// dates or something (2502 or 202506 for 25Q2) or even the mainline bpfloader version...
+// For now this easily suffices for our use case.
+
+struct sdk_level_uint { unsigned int sdk_level; };
+#define SDK_LEVEL_(v) ((struct sdk_level_uint){ .sdk_level = (v) })
+#define SDK_LEVEL_NONE SDK_LEVEL_(0)
+#define SDK_LEVEL_S SDK_LEVEL_(31) // Android 12
+#define SDK_LEVEL_Sv2 SDK_LEVEL_(32) // Android 12L
+#define SDK_LEVEL_T SDK_LEVEL_(33) // Android 13
+#define SDK_LEVEL_U SDK_LEVEL_(34) // Android 14
+#define SDK_LEVEL_V SDK_LEVEL_(35) // Android 15
+#define SDK_LEVEL_24Q3 SDK_LEVEL_V
+#define SDK_LEVEL_25Q2 SDK_LEVEL_(36) // Android 16
+
+#define SDK_LEVEL_IS_AT_LEAST(lvl, v) ((lvl).sdk_level >= (SDK_LEVEL_##v).sdk_level)
+
/*
* BPFFS (ie. /sys/fs/bpf) labelling is as follows:
* subdirectory selinux context mainline usecase / usable by
@@ -168,6 +186,11 @@
* See cs/p:aosp-master%20-file:prebuilts/%20file:genfs_contexts%20"genfscon%20bpf"
*/
+/*
+ * Helper functions called from eBPF programs written in C. These are
+ * implemented in the kernel sources.
+ */
+
/* generic functions */
/*
@@ -231,16 +254,24 @@
(ignore_userdebug).ignore_on_userdebug), \
"bpfloader min version must be >= 0.33 in order to use ignored_on");
+#define ABSOLUTE(x) ((x) < 0 ? -(x) : (x))
+
+#define DEFAULT_BPF_MAP_FLAGS(type, num_entries, mapflags) \
+ ( (mapflags) | \
+ ((num_entries) < 0 ? BPF_F_NO_PREALLOC : 0) | \
+ (type == BPF_MAP_TYPE_LPM_TRIE ? BPF_F_NO_PREALLOC : 0) \
+ )
+
#define DEFINE_BPF_MAP_BASE(the_map, TYPE, keysize, valuesize, num_entries, \
usr, grp, md, selinux, pindir, share, minkver, \
maxkver, minloader, maxloader, ignore_eng, \
- ignore_user, ignore_userdebug) \
+ ignore_user, ignore_userdebug, mapflags) \
const struct bpf_map_def SECTION("maps") the_map = { \
.type = BPF_MAP_TYPE_##TYPE, \
.key_size = (keysize), \
.value_size = (valuesize), \
- .max_entries = (num_entries), \
- .map_flags = 0, \
+ .max_entries = ABSOLUTE(num_entries), \
+ .map_flags = DEFAULT_BPF_MAP_FLAGS(BPF_MAP_TYPE_##TYPE, num_entries, mapflags), \
.uid = (usr), \
.gid = (grp), \
.mode = (md), \
@@ -260,16 +291,17 @@
// Type safe macro to declare a ring buffer and related output functions.
// Compatibility:
// * BPF ring buffers are only available kernels 5.8 and above. Any program
-// accessing the ring buffer should set a program level min_kver >= 5.8.
-// * The definition below sets a map min_kver of 5.8 which requires targeting
+// accessing the ring buffer should set a program level min_kver >= 5.10,
+// since 5.10 is the next LTS version.
+// * The definition below sets a map min_kver of 5.10 which requires targeting
// a BPFLOADER_MIN_VER >= BPFLOADER_S_VERSION.
#define DEFINE_BPF_RINGBUF_EXT(the_map, ValueType, size_bytes, usr, grp, md, \
selinux, pindir, share, min_loader, max_loader, \
ignore_eng, ignore_user, ignore_userdebug) \
DEFINE_BPF_MAP_BASE(the_map, RINGBUF, 0, 0, size_bytes, usr, grp, md, \
- selinux, pindir, share, KVER_5_8, KVER_INF, \
+ selinux, pindir, share, KVER_5_10, KVER_INF, \
min_loader, max_loader, ignore_eng, ignore_user, \
- ignore_userdebug); \
+ ignore_userdebug, 0); \
\
_Static_assert((size_bytes) >= 4096, "min 4 kiB ringbuffer size"); \
_Static_assert((size_bytes) <= 0x10000000, "max 256 MiB ringbuffer size"); \
@@ -317,11 +349,11 @@
/* type safe macro to declare a map and related accessor functions */
#define DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md, \
selinux, pindir, share, min_loader, max_loader, ignore_eng, \
- ignore_user, ignore_userdebug) \
+ ignore_user, ignore_userdebug, mapFlags) \
DEFINE_BPF_MAP_BASE(the_map, TYPE, sizeof(KeyType), sizeof(ValueType), \
num_entries, usr, grp, md, selinux, pindir, share, \
KVER_NONE, KVER_INF, min_loader, max_loader, \
- ignore_eng, ignore_user, ignore_userdebug); \
+ ignore_eng, ignore_user, ignore_userdebug, mapFlags); \
BPF_MAP_ASSERT_OK(BPF_MAP_TYPE_##TYPE, (num_entries), (md)); \
_Static_assert(sizeof(KeyType) < 1024, "aosp/2370288 requires < 1024 byte keys"); \
_Static_assert(sizeof(ValueType) < 65536, "aosp/2370288 requires < 65536 byte values"); \
@@ -359,13 +391,13 @@
#define DEFINE_BPF_MAP_KERNEL_INTERNAL(the_map, TYPE, KeyType, ValueType, num_entries) \
DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, AID_ROOT, AID_ROOT, \
0000, "fs_bpf_loader", "", PRIVATE, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, \
- LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+ LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG, 0)
#define DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md) \
DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md, \
DEFAULT_BPF_MAP_SELINUX_CONTEXT, DEFAULT_BPF_MAP_PIN_SUBDIR, \
PRIVATE, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, \
- LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+ LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG, 0)
#define DEFINE_BPF_MAP(the_map, TYPE, KeyType, ValueType, num_entries) \
DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, \
@@ -387,6 +419,22 @@
DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, \
DEFAULT_BPF_MAP_UID, gid, 0660)
+// idea from Linux include/linux/compiler_types.h (eBPF is always a 64-bit arch)
+#define NATIVE_WORD(t) ((sizeof(t) == 1) || (sizeof(t) == 2) || (sizeof(t) == 4) || (sizeof(t) == 8))
+
+// simplified from Linux include/asm-generic/rwonce.h
+#define READ_ONCE(x) \
+ ({ \
+ _Static_assert(NATIVE_WORD(x), "READ_ONCE requires a native word size"); \
+ (*(const volatile typeof(x) *)&(x)) \
+ })
+
+#define WRITE_ONCE(x, value) \
+ do { \
+ _Static_assert(NATIVE_WORD(x), "WRITE_ONCE requires a native word size"); \
+ *(volatile typeof(x) *)&(x) = (value); \
+ } while (0)
+
// LLVM eBPF builtins: they directly generate BPF_LD_ABS/BPF_LD_IND (skb may be ignored?)
unsigned long long load_byte(void* skb, unsigned long long off) asm("llvm.bpf.load.byte");
unsigned long long load_half(void* skb, unsigned long long off) asm("llvm.bpf.load.half");
diff --git a/bpf/headers/include/bpf_map_def.h b/bpf/headers/include/bpf_map_def.h
index 2d6736c..d67da48 100644
--- a/bpf/headers/include/bpf_map_def.h
+++ b/bpf/headers/include/bpf_map_def.h
@@ -94,6 +94,10 @@
_Static_assert(_Alignof(enum bpf_map_type) == 4, "_Alignof enum bpf_map_type != 4");
// Linux kernel requires sizeof(int) == 4, sizeof(void*) == sizeof(long), sizeof(long long) == 8
+_Static_assert(sizeof(int) == 4, "sizeof int != 4");
+_Static_assert(__alignof__(int) == 4, "__alignof__ int != 4");
+_Static_assert(_Alignof(int) == 4, "_Alignof int != 4");
+
_Static_assert(sizeof(unsigned int) == 4, "sizeof unsigned int != 4");
_Static_assert(__alignof__(unsigned int) == 4, "__alignof__ unsigned int != 4");
_Static_assert(_Alignof(unsigned int) == 4, "_Alignof unsigned int != 4");
@@ -155,7 +159,7 @@
enum bpf_map_type type;
unsigned int key_size;
unsigned int value_size;
- unsigned int max_entries;
+ int max_entries; // negative means BPF_F_NO_PREALLOC, but *might* not work with S
unsigned int map_flags;
// The following are not supported by the Android bpfloader:
diff --git a/bpf/loader/NetBpfLoad.cpp b/bpf/loader/NetBpfLoad.cpp
index ce144a7..04d7492 100644
--- a/bpf/loader/NetBpfLoad.cpp
+++ b/bpf/loader/NetBpfLoad.cpp
@@ -616,9 +616,6 @@
if (type == BPF_MAP_TYPE_DEVMAP || type == BPF_MAP_TYPE_DEVMAP_HASH)
desired_map_flags |= BPF_F_RDONLY_PROG;
- if (type == BPF_MAP_TYPE_LPM_TRIE)
- desired_map_flags |= BPF_F_NO_PREALLOC;
-
// The .h file enforces that this is a power of two, and page size will
// also always be a power of two, so this logic is actually enough to
// force it to be a multiple of the page size, as required by the kernel.
@@ -732,6 +729,12 @@
}
enum bpf_map_type type = md[i].type;
+ if (type == BPF_MAP_TYPE_LPM_TRIE && !isAtLeastKernelVersion(4, 14, 0)) {
+ // On Linux Kernels older than 4.14 this map type doesn't exist - autoskip.
+ ALOGD("skipping LPM_TRIE map %s - requires kver 4.14+", mapNames[i].c_str());
+ mapFds.push_back(unique_fd());
+ continue;
+ }
if (type == BPF_MAP_TYPE_DEVMAP && !isAtLeastKernelVersion(4, 14, 0)) {
// On Linux Kernels older than 4.14 this map type doesn't exist, but it can kind
// of be approximated: ARRAY has the same userspace api, though it is not usable
@@ -794,7 +797,7 @@
.key_size = md[i].key_size,
.value_size = md[i].value_size,
.max_entries = max_entries,
- .map_flags = md[i].map_flags | (type == BPF_MAP_TYPE_LPM_TRIE ? BPF_F_NO_PREALLOC : 0),
+ .map_flags = md[i].map_flags,
};
if (isAtLeastKernelVersion(4, 15, 0))
strlcpy(req.map_name, mapNames[i].c_str(), sizeof(req.map_name));
@@ -1409,17 +1412,15 @@
//
// Also note that 'android_get_device_api_level()' is what the
// //system/core/init/apex_init_util.cpp
- // apex init .XXrc parsing code uses for XX filtering.
- //
- // That code has a hack to bump <35 to 35 (to force aosp/main to parse .35rc),
- // but could (should?) perhaps be adjusted to match this.
- const int effective_api_level = android_get_device_api_level() + (int)unreleased;
- const bool isAtLeastT = (effective_api_level >= __ANDROID_API_T__);
- const bool isAtLeastU = (effective_api_level >= __ANDROID_API_U__);
- const bool isAtLeastV = (effective_api_level >= __ANDROID_API_V__);
- const bool isAtLeastW = (effective_api_level > __ANDROID_API_V__); // TODO: switch to W
+ // apex init .XXrc parsing code uses for XX filtering, and that code
+ // (now) similarly uses __ANDROID_API_FUTURE__ for non 'REL' codenames.
+ const int api_level = unreleased ? __ANDROID_API_FUTURE__ : android_get_device_api_level();
+ const bool isAtLeastT = (api_level >= __ANDROID_API_T__);
+ const bool isAtLeastU = (api_level >= __ANDROID_API_U__);
+ const bool isAtLeastV = (api_level >= __ANDROID_API_V__);
+ const bool isAtLeast25Q2 = (api_level > __ANDROID_API_V__); // TODO: fix >
- const int first_api_level = GetIntProperty("ro.board.first_api_level", effective_api_level);
+ const int first_api_level = GetIntProperty("ro.board.first_api_level", api_level);
// last in U QPR2 beta1
const bool has_platform_bpfloader_rc = exists("/system/etc/init/bpfloader.rc");
@@ -1432,10 +1433,10 @@
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 (isAtLeastW) ++bpfloader_ver; // [47] BPFLOADER_MAINLINE_W_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",
- bpfloader_ver, argv[0], android_get_device_api_level(), effective_api_level,
+ bpfloader_ver, argv[0], android_get_device_api_level(), api_level,
kernelVersion(), describeArch(), getuid(),
has_platform_bpfloader_rc, has_platform_netbpfload_rc);
@@ -1475,6 +1476,13 @@
return 1;
}
+ // 25Q2 bumps the kernel requirement up to 5.4
+ // see also: //system/netd/tests/kernel_test.cpp TestKernel54
+ if (isAtLeast25Q2 && !isAtLeastKernelVersion(5, 4, 0)) {
+ ALOGE("Android 25Q2 requires kernel 5.4.");
+ return 1;
+ }
+
// Technically already required by U, but only enforce on V+
// see also: //system/netd/tests/kernel_test.cpp TestKernel64Bit
if (isAtLeastV && isKernel32Bit() && isAtLeastKernelVersion(5, 16, 0)) {
@@ -1498,13 +1506,13 @@
bool bad = false;
if (!isLtsKernel()) {
- ALOGW("Android V only supports LTS kernels.");
+ ALOGW("Android V+ only supports LTS kernels.");
bad = true;
}
#define REQUIRE(maj, min, sub) \
if (isKernelVersion(maj, min) && !isAtLeastKernelVersion(maj, min, sub)) { \
- ALOGW("Android V requires %d.%d kernel to be %d.%d.%d+.", maj, min, maj, min, sub); \
+ ALOGW("Android V+ requires %d.%d kernel to be %d.%d.%d+.", maj, min, maj, min, sub); \
bad = true; \
}
diff --git a/bpf/netd/BpfHandler.cpp b/bpf/netd/BpfHandler.cpp
index 340acda..6af7228 100644
--- a/bpf/netd/BpfHandler.cpp
+++ b/bpf/netd/BpfHandler.cpp
@@ -36,6 +36,8 @@
using base::unique_fd;
using base::WaitForProperty;
using bpf::getSocketCookie;
+using bpf::isAtLeastKernelVersion;
+using bpf::queryProgram;
using bpf::retrieveProgram;
using netdutils::Status;
using netdutils::statusFromErrno;
@@ -56,7 +58,7 @@
if (!cgroupProg.ok()) {
return statusFromErrno(errno, fmt::format("Failed to get program from {}", programPath));
}
- if (android::bpf::attachProgram(type, cgroupProg, cgroupFd)) {
+ if (bpf::attachProgram(type, cgroupProg, cgroupFd)) {
return statusFromErrno(errno, fmt::format("Program {} attach failed", programPath));
}
return netdutils::status::ok;
@@ -70,6 +72,13 @@
return netdutils::status::ok;
}
+// Checks if the device is running on release version of Android 25Q2 or newer.
+static bool isAtLeast25Q2() {
+ return android_get_device_api_level() >= 36 ||
+ (android_get_device_api_level() == 35 &&
+ modules::sdklevel::detail::IsAtLeastPreReleaseCodename("Baklava"));
+}
+
static Status initPrograms(const char* cg2_path) {
if (!cg2_path) return Status("cg2_path is NULL");
@@ -77,12 +86,12 @@
if (!modules::sdklevel::IsAtLeastT()) return Status("S- platform is unsupported");
// S requires eBPF support which was only added in 4.9, so this should be satisfied.
- if (!bpf::isAtLeastKernelVersion(4, 9, 0)) {
+ if (!isAtLeastKernelVersion(4, 9, 0)) {
return Status("kernel version < 4.9.0 is unsupported");
}
// U bumps the kernel requirement up to 4.14
- if (modules::sdklevel::IsAtLeastU() && !bpf::isAtLeastKernelVersion(4, 14, 0)) {
+ if (modules::sdklevel::IsAtLeastU() && !isAtLeastKernelVersion(4, 14, 0)) {
return Status("U+ platform with kernel version < 4.14.0 is unsupported");
}
@@ -91,13 +100,19 @@
return Status("U+ platform with cg2_path != /sys/fs/cgroup is unsupported");
}
- unique_fd cg_fd(open(cg2_path, O_DIRECTORY | O_RDONLY | O_CLOEXEC));
- if (!cg_fd.ok()) {
- const int err = errno;
- ALOGE("Failed to open the cgroup directory: %s", strerror(err));
- return statusFromErrno(err, "Open the cgroup directory failed");
+ // V bumps the kernel requirement up to 4.19
+ if (modules::sdklevel::IsAtLeastV() && !isAtLeastKernelVersion(4, 19, 0)) {
+ return Status("V+ platform with kernel version < 4.19.0 is unsupported");
}
+ // 25Q2 bumps the kernel requirement up to 5.4
+ if (isAtLeast25Q2() && !isAtLeastKernelVersion(5, 4, 0)) {
+ return Status("25Q2+ platform with kernel version < 5.4.0 is unsupported");
+ }
+
+ unique_fd cg_fd(open(cg2_path, O_DIRECTORY | O_RDONLY | O_CLOEXEC));
+ if (!cg_fd.ok()) return statusFromErrno(errno, "Opening cgroup dir failed");
+
RETURN_IF_NOT_OK(checkProgramAccessible(XT_BPF_ALLOWLIST_PROG_PATH));
RETURN_IF_NOT_OK(checkProgramAccessible(XT_BPF_DENYLIST_PROG_PATH));
RETURN_IF_NOT_OK(checkProgramAccessible(XT_BPF_EGRESS_PROG_PATH));
@@ -110,12 +125,12 @@
// cgroup if the program is pinned properly.
// TODO: delete the if statement once all devices should support cgroup
// socket filter (ie. the minimum kernel version required is 4.14).
- if (bpf::isAtLeastKernelVersion(4, 14, 0)) {
+ if (isAtLeastKernelVersion(4, 14, 0)) {
RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_INET_CREATE_PROG_PATH,
cg_fd, BPF_CGROUP_INET_SOCK_CREATE));
}
- if (bpf::isAtLeastKernelVersion(5, 10, 0)) {
+ if (isAtLeastKernelVersion(5, 10, 0)) {
RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_INET_RELEASE_PROG_PATH,
cg_fd, BPF_CGROUP_INET_SOCK_RELEASE));
}
@@ -123,7 +138,7 @@
if (modules::sdklevel::IsAtLeastV()) {
// V requires 4.19+, so technically this 2nd 'if' is not required, but it
// doesn't hurt us to try to support AOSP forks that try to support older kernels.
- if (bpf::isAtLeastKernelVersion(4, 19, 0)) {
+ if (isAtLeastKernelVersion(4, 19, 0)) {
RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_CONNECT4_PROG_PATH,
cg_fd, BPF_CGROUP_INET4_CONNECT));
RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_CONNECT6_PROG_PATH,
@@ -138,7 +153,7 @@
cg_fd, BPF_CGROUP_UDP6_SENDMSG));
}
- if (bpf::isAtLeastKernelVersion(5, 4, 0)) {
+ if (isAtLeastKernelVersion(5, 4, 0)) {
RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_GETSOCKOPT_PROG_PATH,
cg_fd, BPF_CGROUP_GETSOCKOPT));
RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_SETSOCKOPT_PROG_PATH,
@@ -146,7 +161,7 @@
}
}
- if (bpf::isAtLeastKernelVersion(4, 19, 0)) {
+ if (isAtLeastKernelVersion(4, 19, 0)) {
RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_BIND4_PROG_PATH,
cg_fd, BPF_CGROUP_INET4_BIND));
RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_BIND6_PROG_PATH,
@@ -154,32 +169,32 @@
// This should trivially pass, since we just attached up above,
// but BPF_PROG_QUERY is only implemented on 4.19+ kernels.
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_EGRESS) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_INGRESS) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_SOCK_CREATE) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET4_BIND) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET6_BIND) <= 0) abort();
+ if (queryProgram(cg_fd, BPF_CGROUP_INET_EGRESS) <= 0) abort();
+ if (queryProgram(cg_fd, BPF_CGROUP_INET_INGRESS) <= 0) abort();
+ if (queryProgram(cg_fd, BPF_CGROUP_INET_SOCK_CREATE) <= 0) abort();
+ if (queryProgram(cg_fd, BPF_CGROUP_INET4_BIND) <= 0) abort();
+ if (queryProgram(cg_fd, BPF_CGROUP_INET6_BIND) <= 0) abort();
}
- if (bpf::isAtLeastKernelVersion(5, 10, 0)) {
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_SOCK_RELEASE) <= 0) abort();
+ if (isAtLeastKernelVersion(5, 10, 0)) {
+ if (queryProgram(cg_fd, BPF_CGROUP_INET_SOCK_RELEASE) <= 0) abort();
}
if (modules::sdklevel::IsAtLeastV()) {
// V requires 4.19+, so technically this 2nd 'if' is not required, but it
// doesn't hurt us to try to support AOSP forks that try to support older kernels.
- if (bpf::isAtLeastKernelVersion(4, 19, 0)) {
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET4_CONNECT) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET6_CONNECT) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP4_RECVMSG) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP6_RECVMSG) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP4_SENDMSG) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP6_SENDMSG) <= 0) abort();
+ if (isAtLeastKernelVersion(4, 19, 0)) {
+ if (queryProgram(cg_fd, BPF_CGROUP_INET4_CONNECT) <= 0) abort();
+ if (queryProgram(cg_fd, BPF_CGROUP_INET6_CONNECT) <= 0) abort();
+ if (queryProgram(cg_fd, BPF_CGROUP_UDP4_RECVMSG) <= 0) abort();
+ if (queryProgram(cg_fd, BPF_CGROUP_UDP6_RECVMSG) <= 0) abort();
+ if (queryProgram(cg_fd, BPF_CGROUP_UDP4_SENDMSG) <= 0) abort();
+ if (queryProgram(cg_fd, BPF_CGROUP_UDP6_SENDMSG) <= 0) abort();
}
- if (bpf::isAtLeastKernelVersion(5, 4, 0)) {
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_GETSOCKOPT) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_SETSOCKOPT) <= 0) abort();
+ if (isAtLeastKernelVersion(5, 4, 0)) {
+ if (queryProgram(cg_fd, BPF_CGROUP_GETSOCKOPT) <= 0) abort();
+ if (queryProgram(cg_fd, BPF_CGROUP_SETSOCKOPT) <= 0) abort();
}
}
@@ -219,7 +234,7 @@
// but there could be platform provided (xt_)bpf programs that oem/vendor
// modified netd (which calls us during init) depends on...
ALOGI("Waiting for platform BPF programs");
- android::bpf::waitForProgsLoaded();
+ bpf::waitForProgsLoaded();
}
if (!mainlineNetBpfLoadDone()) {
@@ -257,6 +272,16 @@
RETURN_IF_NOT_OK(initPrograms(cg2_path));
RETURN_IF_NOT_OK(initMaps());
+ if (android_get_device_api_level() > __ANDROID_API_V__) {
+ // make sure netd can create & write maps. sepolicy is V+, but enough to enforce on 25Q2+
+ int key = 1;
+ int value = 123;
+ unique_fd map(bpf::createMap(BPF_MAP_TYPE_ARRAY, sizeof(key), sizeof(value), 2, 0));
+ if (!map.ok()) return statusFromErrno(errno, fmt::format("map create failed"));
+ int rv = bpf::writeToMapEntry(map, &key, &value, BPF_ANY);
+ if (rv) return statusFromErrno(errno, fmt::format("map write failed (rv={})", rv));
+ }
+
return netdutils::status::ok;
}
@@ -283,7 +308,7 @@
Status BpfHandler::initMaps() {
// bpfLock() requires bpfGetFdMapId which is only available on 4.14+ kernels.
- if (bpf::isAtLeastKernelVersion(4, 14, 0)) {
+ if (isAtLeastKernelVersion(4, 14, 0)) {
mapLockTest();
}
@@ -337,7 +362,7 @@
return -errno;
}
if (socketFamily != AF_INET && socketFamily != AF_INET6) {
- ALOGE("Unsupported family: %d", socketFamily);
+ ALOGV("Unsupported family: %d", socketFamily);
return -EAFNOSUPPORT;
}
@@ -348,7 +373,7 @@
return -errno;
}
if (socketProto != IPPROTO_UDP && socketProto != IPPROTO_TCP) {
- ALOGE("Unsupported protocol: %d", socketProto);
+ ALOGV("Unsupported protocol: %d", socketProto);
return -EPROTONOSUPPORT;
}
@@ -410,8 +435,8 @@
ALOGE("Failed to tag the socket: %s", strerror(res.error().code()));
return -res.error().code();
}
- ALOGD("Socket with cookie %" PRIu64 " tagged successfully with tag %" PRIu32 " uid %u "
- "and real uid %u", sock_cookie, tag, chargeUid, realUid);
+ ALOGV("Socket with cookie %" PRIu64 " tagged successfully with tag %" PRIu32 " uid %u "
+ "and real uid %u", sock_cookie, tag, chargeUid, realUid);
return 0;
}
@@ -422,10 +447,11 @@
if (!mCookieTagMap.isValid()) return -EPERM;
base::Result<void> res = mCookieTagMap.deleteValue(sock_cookie);
if (!res.ok()) {
- ALOGE("Failed to untag socket: %s", strerror(res.error().code()));
- return -res.error().code();
+ const int err = res.error().code();
+ if (err != ENOENT) ALOGE("Failed to untag socket: %s", strerror(err));
+ return -err;
}
- ALOGD("Socket with cookie %" PRIu64 " untagged successfully.", sock_cookie);
+ ALOGV("Socket with cookie %" PRIu64 " untagged successfully.", sock_cookie);
return 0;
}
diff --git a/bpf/progs/bpf_net_helpers.h b/bpf/progs/bpf_net_helpers.h
index a5664ba..4085ed4 100644
--- a/bpf/progs/bpf_net_helpers.h
+++ b/bpf/progs/bpf_net_helpers.h
@@ -84,6 +84,8 @@
#define ETH_IP6_TCP_OFFSET(field) (ETH_HLEN + IP6_TCP_OFFSET(field))
#define ETH_IP6_UDP_OFFSET(field) (ETH_HLEN + IP6_UDP_OFFSET(field))
+static uint64_t (*bpf_get_netns_cookie)(void* ctx) = (void*)BPF_FUNC_get_netns_cookie;
+
// this returns 0 iff skb->sk is NULL
static uint64_t (*bpf_get_socket_cookie)(struct __sk_buff* skb) = (void*)BPF_FUNC_get_socket_cookie;
static uint64_t (*bpf_get_sk_cookie)(struct bpf_sock* sk) = (void*)BPF_FUNC_get_socket_cookie;
diff --git a/bpf/progs/netd.c b/bpf/progs/netd.c
index ed0eed5..08635b3 100644
--- a/bpf/progs/netd.c
+++ b/bpf/progs/netd.c
@@ -25,10 +25,6 @@
static const int PASS = 1;
static const int DROP_UNLESS_DNS = 2; // internal to our program
-// Used for 'bool enable_tracing'
-static const bool TRACE_ON = true;
-static const bool TRACE_OFF = false;
-
// offsetof(struct iphdr, ihl) -- but that's a bitfield
#define IPPROTO_IHL_OFF 0
@@ -46,14 +42,14 @@
DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, \
AID_ROOT, AID_NET_BW_ACCT, 0060, "fs_bpf_net_shared", "", \
PRIVATE, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, \
- LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+ LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG, 0)
// For maps netd only needs read only access to
#define DEFINE_BPF_MAP_RO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, \
AID_ROOT, AID_NET_BW_ACCT, 0460, "fs_bpf_netd_readonly", "", \
PRIVATE, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, \
- LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+ LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG, 0)
// For maps netd needs to be able to read and write
#define DEFINE_BPF_MAP_RW_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
@@ -92,7 +88,7 @@
DEFINE_BPF_MAP_EXT(packet_trace_enabled_map, ARRAY, uint32_t, bool, 1,
AID_ROOT, AID_SYSTEM, 0060, "fs_bpf_net_shared", "", PRIVATE,
BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG,
- LOAD_ON_USER, LOAD_ON_USERDEBUG)
+ LOAD_ON_USER, LOAD_ON_USERDEBUG, 0)
// A ring buffer on which packet information is pushed.
DEFINE_BPF_RINGBUF_EXT(packet_trace_ringbuf, PacketTrace, PACKET_TRACE_BUF_SIZE,
@@ -103,6 +99,17 @@
DEFINE_BPF_MAP_RO_NETD(data_saver_enabled_map, ARRAY, uint32_t, bool,
DATA_SAVER_ENABLED_MAP_SIZE)
+DEFINE_BPF_MAP_EXT(local_net_access_map, LPM_TRIE, LocalNetAccessKey, bool, 1000,
+ AID_ROOT, AID_NET_BW_ACCT, 0060, "fs_bpf_net_shared", "", PRIVATE,
+ BPFLOADER_MAINLINE_25Q2_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG, LOAD_ON_USER,
+ LOAD_ON_USERDEBUG, 0)
+
+// not preallocated
+DEFINE_BPF_MAP_EXT(local_net_blocked_uid_map, HASH, uint32_t, bool, -1000,
+ AID_ROOT, AID_NET_BW_ACCT, 0060, "fs_bpf_net_shared", "", PRIVATE,
+ BPFLOADER_MAINLINE_25Q2_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG, LOAD_ON_USER,
+ LOAD_ON_USERDEBUG, 0)
+
// iptables xt_bpf programs need to be usable by both netd and netutils_wrappers
// selinux contexts, because even non-xt_bpf iptables mutations are implemented as
// a full table dump, followed by an update in userspace, and then a reload into the kernel,
@@ -110,31 +117,34 @@
// program (see XT_BPF_MODE_PATH_PINNED) and then the iptables binary (or rather
// the kernel acting on behalf of it) must be able to retrieve the pinned program
// for the reload to succeed
-#define DEFINE_XTBPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
- DEFINE_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog)
+#define DEFINE_XTBPF_PROG(SECTION_NAME, the_prog) \
+ DEFINE_BPF_PROG(SECTION_NAME, AID_ROOT, AID_NET_ADMIN, the_prog)
// programs that need to be usable by netd, but not by netutils_wrappers
// (this is because these are currently attached by the mainline provided libnetd_updatable .so
// which is loaded into netd and thus runs as netd uid/gid/selinux context)
-#define DEFINE_NETD_BPF_PROG_KVER_RANGE(SECTION_NAME, prog_uid, prog_gid, the_prog, minKV, maxKV) \
- DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, \
- minKV, maxKV, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, MANDATORY, \
+#define DEFINE_NETD_BPF_PROG_RANGES(SECTION_NAME, the_prog, minKV, maxKV, min_loader, max_loader) \
+ DEFINE_BPF_PROG_EXT(SECTION_NAME, AID_ROOT, AID_ROOT, the_prog, \
+ minKV, maxKV, min_loader, max_loader, MANDATORY, \
"fs_bpf_netd_readonly", "", LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
-#define DEFINE_NETD_BPF_PROG_KVER(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv) \
- DEFINE_NETD_BPF_PROG_KVER_RANGE(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, KVER_INF)
+#define DEFINE_NETD_BPF_PROG_KVER_RANGE(SECTION_NAME, the_prog, minKV, maxKV) \
+ DEFINE_NETD_BPF_PROG_RANGES(SECTION_NAME, the_prog, minKV, maxKV, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER)
-#define DEFINE_NETD_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
- DEFINE_NETD_BPF_PROG_KVER(SECTION_NAME, prog_uid, prog_gid, the_prog, KVER_NONE)
+#define DEFINE_NETD_BPF_PROG_KVER(SECTION_NAME, the_prog, min_kv) \
+ DEFINE_NETD_BPF_PROG_KVER_RANGE(SECTION_NAME, the_prog, min_kv, KVER_INF)
-#define DEFINE_NETD_V_BPF_PROG_KVER(SECTION_NAME, prog_uid, prog_gid, the_prog, minKV) \
- DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, minKV, \
+#define DEFINE_NETD_BPF_PROG(SECTION_NAME, the_prog) \
+ DEFINE_NETD_BPF_PROG_KVER(SECTION_NAME, the_prog, KVER_NONE)
+
+#define DEFINE_NETD_V_BPF_PROG_KVER(SECTION_NAME, the_prog, minKV) \
+ DEFINE_BPF_PROG_EXT(SECTION_NAME, AID_ROOT, AID_ROOT, the_prog, minKV, \
KVER_INF, BPFLOADER_MAINLINE_V_VERSION, BPFLOADER_MAX_VER, MANDATORY, \
"fs_bpf_netd_readonly", "", LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
// programs that only need to be usable by the system server
-#define DEFINE_SYS_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
- DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, KVER_NONE, KVER_INF, \
+#define DEFINE_SYS_BPF_PROG(SECTION_NAME, the_prog) \
+ DEFINE_BPF_PROG_EXT(SECTION_NAME, AID_ROOT, AID_NET_ADMIN, the_prog, KVER_NONE, KVER_INF, \
BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, MANDATORY, \
"fs_bpf_net_shared", "", LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
@@ -231,11 +241,70 @@
: bpf_skb_load_bytes(skb, L3_off, to, len);
}
+// False iff arguments are found with longest prefix match lookup and disallowed.
+static inline __always_inline bool is_local_net_access_allowed(const uint32_t if_index,
+ const struct in6_addr* remote_ip6, const uint16_t protocol, const __be16 remote_port) {
+ LocalNetAccessKey query_key = {
+ .lpm_bitlen = 8 * (sizeof(if_index) + sizeof(*remote_ip6) + sizeof(protocol)
+ + sizeof(remote_port)),
+ .if_index = if_index,
+ .remote_ip6 = *remote_ip6,
+ .protocol = protocol,
+ .remote_port = remote_port
+ };
+ bool* v = bpf_local_net_access_map_lookup_elem(&query_key);
+ return v ? *v : true;
+}
+
+static __always_inline inline bool should_block_local_network_packets(struct __sk_buff *skb,
+ const uint32_t uid, const struct egress_bool egress,
+ const struct kver_uint kver) {
+ if (is_system_uid(uid)) return false;
+
+ bool* block_local_net = bpf_local_net_blocked_uid_map_lookup_elem(&uid);
+ if (!block_local_net) return false; // uid not found in map
+ if (!*block_local_net) return false; // lookup returned 'bool false'
+
+ struct in6_addr remote_ip6;
+ uint8_t ip_proto;
+ uint8_t L4_off;
+ if (skb->protocol == htons(ETH_P_IP)) {
+ int remote_ip_ofs = egress.egress ? IP4_OFFSET(daddr) : IP4_OFFSET(saddr);
+ remote_ip6.s6_addr32[0] = 0;
+ remote_ip6.s6_addr32[1] = 0;
+ remote_ip6.s6_addr32[2] = htonl(0xFFFF);
+ (void)bpf_skb_load_bytes_net(skb, remote_ip_ofs, &remote_ip6.s6_addr32[3], 4, kver);
+ (void)bpf_skb_load_bytes_net(skb, IP4_OFFSET(protocol), &ip_proto, sizeof(ip_proto), kver);
+ uint8_t ihl;
+ (void)bpf_skb_load_bytes_net(skb, IPPROTO_IHL_OFF, &ihl, sizeof(ihl), kver);
+ L4_off = (ihl & 0x0F) * 4; // IHL calculation.
+ } else if (skb->protocol == htons(ETH_P_IPV6)) {
+ int remote_ip_ofs = egress.egress ? IP6_OFFSET(daddr) : IP6_OFFSET(saddr);
+ (void)bpf_skb_load_bytes_net(skb, remote_ip_ofs, &remote_ip6, sizeof(remote_ip6), kver);
+ (void)bpf_skb_load_bytes_net(skb, IP6_OFFSET(nexthdr), &ip_proto, sizeof(ip_proto), kver);
+ L4_off = sizeof(struct ipv6hdr);
+ } else {
+ return false;
+ }
+
+ __be16 remote_port = 0;
+ switch (ip_proto) {
+ case IPPROTO_TCP:
+ case IPPROTO_DCCP:
+ case IPPROTO_UDP:
+ case IPPROTO_UDPLITE:
+ case IPPROTO_SCTP:
+ (void)bpf_skb_load_bytes_net(skb, L4_off + (egress.egress ? 2 : 0), &remote_port, sizeof(remote_port), kver);
+ break;
+ }
+
+ return !is_local_net_access_allowed(skb->ifindex, &remote_ip6, ip_proto, remote_port);
+}
+
static __always_inline inline void do_packet_tracing(
const struct __sk_buff* const skb, const struct egress_bool egress, const uint32_t uid,
- const uint32_t tag, const bool enable_tracing, const struct kver_uint kver) {
- if (!enable_tracing) return;
- if (!KVER_IS_AT_LEAST(kver, 5, 8, 0)) return;
+ const uint32_t tag, const struct kver_uint kver) {
+ if (!KVER_IS_AT_LEAST(kver, 5, 10, 0)) return;
uint32_t mapKey = 0;
bool* traceConfig = bpf_packet_trace_enabled_map_lookup_elem(&mapKey);
@@ -393,7 +462,8 @@
static __always_inline inline int bpf_owner_match(struct __sk_buff* skb, uint32_t uid,
const struct egress_bool egress,
- const struct kver_uint kver) {
+ const struct kver_uint kver,
+ const struct sdk_level_uint lvl) {
if (is_system_uid(uid)) return PASS;
if (skip_owner_match(skb, egress, kver)) return PASS;
@@ -423,6 +493,11 @@
return DROP_UNLESS_DNS;
}
}
+
+ if (SDK_LEVEL_IS_AT_LEAST(lvl, 25Q2) && skb->ifindex == 1) {
+ // TODO: sdksandbox localhost restrictions
+ }
+
return PASS;
}
@@ -440,8 +515,8 @@
static __always_inline inline int bpf_traffic_account(struct __sk_buff* skb,
const struct egress_bool egress,
- const bool enable_tracing,
- const struct kver_uint kver) {
+ const struct kver_uint kver,
+ const struct sdk_level_uint lvl) {
// sock_uid will be 'overflowuid' if !sk_fullsock(sk_to_full_sk(skb->sk))
uint32_t sock_uid = bpf_get_socket_uid(skb);
@@ -470,7 +545,7 @@
// CLAT daemon receives via an untagged AF_PACKET socket.
if (egress.egress && uid == AID_CLAT) return PASS;
- int match = bpf_owner_match(skb, sock_uid, egress, kver);
+ int match = bpf_owner_match(skb, sock_uid, egress, kver, lvl);
// Workaround for secureVPN with VpnIsolation enabled, refer to b/159994981 for details.
// Keep TAG_SYSTEM_DNS in sync with DnsResolver/include/netd_resolv/resolv.h
@@ -483,6 +558,10 @@
if (match == DROP_UNLESS_DNS) match = DROP;
}
+ if (SDK_LEVEL_IS_AT_LEAST(lvl, 25Q2) && (match != DROP)) {
+ if (should_block_local_network_packets(skb, uid, egress, kver)) match = DROP;
+ }
+
// If an outbound packet is going to be dropped, we do not count that traffic.
if (egress.egress && (match == DROP)) return DROP;
@@ -496,7 +575,7 @@
if (!selectedMap) return PASS; // cannot happen, needed to keep bpf verifier happy
- do_packet_tracing(skb, egress, uid, tag, enable_tracing, kver);
+ do_packet_tracing(skb, egress, uid, tag, kver);
update_stats_with_config(*selectedMap, skb, &key, egress, kver);
update_app_uid_stats_map(skb, &uid, egress, kver);
@@ -509,52 +588,104 @@
return match;
}
-// Tracing on Android U+ 5.8+
-DEFINE_BPF_PROG_EXT("cgroupskb/ingress/stats$trace", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_ingress_trace, KVER_5_8, KVER_INF,
- BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, MANDATORY,
- "fs_bpf_netd_readonly", "",
- LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+// -----
+
+// Supported kernel + platform/os version combinations:
+//
+// | 4.9 | 4.14 | 4.19 | 5.4 | 5.10 | 5.15 | 6.1 | 6.6 | 6.12 |
+// 25Q2 | | | | x | x | x | x | x | x |
+// V | | | x | x | x | x | x | x | | (netbpfload)
+// U | | x | x | x | x | x | x | | |
+// T | x | x | x | x | x | x | | | | (magic netbpfload)
+// S | x | x | x | x | x | | | | | (platform loads offload)
+// R | x | x | x | x | | | | | | (no mainline ebpf)
+//
+// Not relevant for eBPF, but R can also run on 4.4
+
+// ----- cgroupskb/ingress/stats -----
+
+// Android 25Q2+ 5.10+ (localnet protection + tracing)
+DEFINE_NETD_BPF_PROG_RANGES("cgroupskb/ingress/stats$5_10_25q2",
+ bpf_cgroup_ingress_5_10_25q2, KVER_5_10, KVER_INF,
+ BPFLOADER_MAINLINE_25Q2_VERSION, BPFLOADER_MAX_VER)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, INGRESS, TRACE_ON, KVER_5_8);
+ return bpf_traffic_account(skb, INGRESS, KVER_5_10, SDK_LEVEL_25Q2);
}
-DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/ingress/stats$4_19", AID_ROOT, AID_SYSTEM,
+// Android 25Q2+ 5.4 (localnet protection)
+DEFINE_NETD_BPF_PROG_RANGES("cgroupskb/ingress/stats$5_4_25q2",
+ bpf_cgroup_ingress_5_4_25q2, KVER_5_4, KVER_5_10,
+ BPFLOADER_MAINLINE_25Q2_VERSION, BPFLOADER_MAX_VER)
+(struct __sk_buff* skb) {
+ return bpf_traffic_account(skb, INGRESS, KVER_5_4, SDK_LEVEL_25Q2);
+}
+
+// Android U/V 5.10+ (tracing)
+DEFINE_NETD_BPF_PROG_RANGES("cgroupskb/ingress/stats$5_10_u",
+ bpf_cgroup_ingress_5_10_u, KVER_5_10, KVER_INF,
+ BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAINLINE_25Q2_VERSION)
+(struct __sk_buff* skb) {
+ return bpf_traffic_account(skb, INGRESS, KVER_5_10, SDK_LEVEL_U);
+}
+
+// Android T/U/V 4.19 & T/U/V/25Q2 5.4 & T 5.10/5.15
+DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/ingress/stats$4_19",
bpf_cgroup_ingress_4_19, KVER_4_19, KVER_INF)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, INGRESS, TRACE_OFF, KVER_4_19);
+ return bpf_traffic_account(skb, INGRESS, KVER_4_19, SDK_LEVEL_T);
}
-DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/ingress/stats$4_14", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_ingress_4_14, KVER_NONE, KVER_4_19)
+// Android T 4.9 & T/U 4.14
+DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/ingress/stats$4_9",
+ bpf_cgroup_ingress_4_9, KVER_NONE, KVER_4_19)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, INGRESS, TRACE_OFF, KVER_NONE);
+ return bpf_traffic_account(skb, INGRESS, KVER_NONE, SDK_LEVEL_T);
}
-// Tracing on Android U+ 5.8+
-DEFINE_BPF_PROG_EXT("cgroupskb/egress/stats$trace", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_egress_trace, KVER_5_8, KVER_INF,
- BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, MANDATORY,
- "fs_bpf_netd_readonly", "",
- LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+// ----- cgroupskb/egress/stats -----
+
+// Android 25Q2+ 5.10+ (localnet protection + tracing)
+DEFINE_NETD_BPF_PROG_RANGES("cgroupskb/egress/stats$5_10_25q2",
+ bpf_cgroup_egress_5_10_25q2, KVER_5_10, KVER_INF,
+ BPFLOADER_MAINLINE_25Q2_VERSION, BPFLOADER_MAX_VER)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER_5_8);
+ return bpf_traffic_account(skb, EGRESS, KVER_5_10, SDK_LEVEL_25Q2);
}
-DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/egress/stats$4_19", AID_ROOT, AID_SYSTEM,
+// Android 25Q2+ 5.4 (localnet protection)
+DEFINE_NETD_BPF_PROG_RANGES("cgroupskb/egress/stats$5_4_25q2",
+ bpf_cgroup_egress_5_4_25q2, KVER_5_4, KVER_5_10,
+ BPFLOADER_MAINLINE_25Q2_VERSION, BPFLOADER_MAX_VER)
+(struct __sk_buff* skb) {
+ return bpf_traffic_account(skb, EGRESS, KVER_5_4, SDK_LEVEL_25Q2);
+}
+
+// Android U/V 5.10+ (tracing)
+DEFINE_NETD_BPF_PROG_RANGES("cgroupskb/egress/stats$5_10_u",
+ bpf_cgroup_egress_5_10_u, KVER_5_10, KVER_INF,
+ BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAINLINE_25Q2_VERSION)
+(struct __sk_buff* skb) {
+ return bpf_traffic_account(skb, EGRESS, KVER_5_10, SDK_LEVEL_U);
+}
+
+// Android T/U/V 4.19 & T/U/V/25Q2 5.4 & T 5.10/5.15
+DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/egress/stats$4_19",
bpf_cgroup_egress_4_19, KVER_4_19, KVER_INF)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, EGRESS, TRACE_OFF, KVER_4_19);
+ return bpf_traffic_account(skb, EGRESS, KVER_4_19, SDK_LEVEL_T);
}
-DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/egress/stats$4_14", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_egress_4_14, KVER_NONE, KVER_4_19)
+// Android T 4.9 & T/U 4.14
+DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/egress/stats$4_9",
+ bpf_cgroup_egress_4_9, KVER_NONE, KVER_4_19)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, EGRESS, TRACE_OFF, KVER_NONE);
+ return bpf_traffic_account(skb, EGRESS, KVER_NONE, SDK_LEVEL_T);
}
+// -----
+
// WARNING: Android T's non-updatable netd depends on the name of this program.
-DEFINE_XTBPF_PROG("skfilter/egress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_egress_prog)
+DEFINE_XTBPF_PROG("skfilter/egress/xtbpf", xt_bpf_egress_prog)
(struct __sk_buff* skb) {
// Clat daemon does not generate new traffic, all its traffic is accounted for already
// on the v4-* interfaces (except for the 20 (or 28) extra bytes of IPv6 vs IPv4 overhead,
@@ -573,7 +704,7 @@
}
// WARNING: Android T's non-updatable netd depends on the name of this program.
-DEFINE_XTBPF_PROG("skfilter/ingress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_ingress_prog)
+DEFINE_XTBPF_PROG("skfilter/ingress/xtbpf", xt_bpf_ingress_prog)
(struct __sk_buff* skb) {
// Clat daemon traffic is not accounted by virtue of iptables raw prerouting drop rule
// (in clat_raw_PREROUTING chain), which triggers before this (in bw_raw_PREROUTING chain).
@@ -585,7 +716,7 @@
return XTBPF_MATCH;
}
-DEFINE_SYS_BPF_PROG("schedact/ingress/account", AID_ROOT, AID_NET_ADMIN,
+DEFINE_SYS_BPF_PROG("schedact/ingress/account",
tc_bpf_ingress_account_prog)
(struct __sk_buff* skb) {
if (is_received_skb(skb)) {
@@ -597,7 +728,7 @@
}
// WARNING: Android T's non-updatable netd depends on the name of this program.
-DEFINE_XTBPF_PROG("skfilter/allowlist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_allowlist_prog)
+DEFINE_XTBPF_PROG("skfilter/allowlist/xtbpf", xt_bpf_allowlist_prog)
(struct __sk_buff* skb) {
uint32_t sock_uid = bpf_get_socket_uid(skb);
if (is_system_uid(sock_uid)) return XTBPF_MATCH;
@@ -616,7 +747,7 @@
}
// WARNING: Android T's non-updatable netd depends on the name of this program.
-DEFINE_XTBPF_PROG("skfilter/denylist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_denylist_prog)
+DEFINE_XTBPF_PROG("skfilter/denylist/xtbpf", xt_bpf_denylist_prog)
(struct __sk_buff* skb) {
uint32_t sock_uid = bpf_get_socket_uid(skb);
UidOwnerValue* denylistMatch = bpf_uid_owner_map_lookup_elem(&sock_uid);
@@ -639,14 +770,12 @@
return permissions ? *permissions : BPF_PERMISSION_INTERNET;
}
-DEFINE_NETD_BPF_PROG_KVER("cgroupsock/inet_create", AID_ROOT, AID_ROOT, inet_socket_create,
- KVER_4_14)
+DEFINE_NETD_BPF_PROG_KVER("cgroupsock/inet_create", inet_socket_create, KVER_4_14)
(__unused struct bpf_sock* sk) {
return (get_app_permissions() & BPF_PERMISSION_INTERNET) ? BPF_ALLOW : BPF_DISALLOW;
}
-DEFINE_NETD_BPF_PROG_KVER("cgroupsockrelease/inet_release", AID_ROOT, AID_ROOT,
- inet_socket_release, KVER_5_10)
+DEFINE_NETD_BPF_PROG_KVER("cgroupsockrelease/inet_release", inet_socket_release, KVER_5_10)
(struct bpf_sock* sk) {
uint64_t cookie = bpf_get_sk_cookie(sk);
if (cookie) bpf_cookie_tag_map_delete_elem(&cookie);
@@ -699,47 +828,47 @@
return BPF_ALLOW;
}
-DEFINE_NETD_BPF_PROG_KVER("bind4/inet4_bind", AID_ROOT, AID_ROOT, inet4_bind, KVER_4_19)
+DEFINE_NETD_BPF_PROG_KVER("bind4/inet4_bind", inet4_bind, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return block_port(ctx);
}
-DEFINE_NETD_BPF_PROG_KVER("bind6/inet6_bind", AID_ROOT, AID_ROOT, inet6_bind, KVER_4_19)
+DEFINE_NETD_BPF_PROG_KVER("bind6/inet6_bind", inet6_bind, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return block_port(ctx);
}
-DEFINE_NETD_V_BPF_PROG_KVER("connect4/inet4_connect", AID_ROOT, AID_ROOT, inet4_connect, KVER_4_19)
+DEFINE_NETD_V_BPF_PROG_KVER("connect4/inet4_connect", inet4_connect, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return check_localhost(ctx);
}
-DEFINE_NETD_V_BPF_PROG_KVER("connect6/inet6_connect", AID_ROOT, AID_ROOT, inet6_connect, KVER_4_19)
+DEFINE_NETD_V_BPF_PROG_KVER("connect6/inet6_connect", inet6_connect, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return check_localhost(ctx);
}
-DEFINE_NETD_V_BPF_PROG_KVER("recvmsg4/udp4_recvmsg", AID_ROOT, AID_ROOT, udp4_recvmsg, KVER_4_19)
+DEFINE_NETD_V_BPF_PROG_KVER("recvmsg4/udp4_recvmsg", udp4_recvmsg, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return check_localhost(ctx);
}
-DEFINE_NETD_V_BPF_PROG_KVER("recvmsg6/udp6_recvmsg", AID_ROOT, AID_ROOT, udp6_recvmsg, KVER_4_19)
+DEFINE_NETD_V_BPF_PROG_KVER("recvmsg6/udp6_recvmsg", udp6_recvmsg, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return check_localhost(ctx);
}
-DEFINE_NETD_V_BPF_PROG_KVER("sendmsg4/udp4_sendmsg", AID_ROOT, AID_ROOT, udp4_sendmsg, KVER_4_19)
+DEFINE_NETD_V_BPF_PROG_KVER("sendmsg4/udp4_sendmsg", udp4_sendmsg, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return check_localhost(ctx);
}
-DEFINE_NETD_V_BPF_PROG_KVER("sendmsg6/udp6_sendmsg", AID_ROOT, AID_ROOT, udp6_sendmsg, KVER_4_19)
+DEFINE_NETD_V_BPF_PROG_KVER("sendmsg6/udp6_sendmsg", udp6_sendmsg, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return check_localhost(ctx);
}
-DEFINE_NETD_V_BPF_PROG_KVER("getsockopt/prog", AID_ROOT, AID_ROOT, getsockopt_prog, KVER_5_4)
+DEFINE_NETD_V_BPF_PROG_KVER("getsockopt/prog", getsockopt_prog, KVER_5_4)
(struct bpf_sockopt *ctx) {
// Tell kernel to return 'original' kernel reply (instead of the bpf modified buffer)
// This is important if the answer is larger than PAGE_SIZE (max size this bpf hook can provide)
@@ -747,7 +876,7 @@
return BPF_ALLOW;
}
-DEFINE_NETD_V_BPF_PROG_KVER("setsockopt/prog", AID_ROOT, AID_ROOT, setsockopt_prog, KVER_5_4)
+DEFINE_NETD_V_BPF_PROG_KVER("setsockopt/prog", setsockopt_prog, KVER_5_4)
(struct bpf_sockopt *ctx) {
// Tell kernel to use/process original buffer provided by userspace.
// This is important if it is larger than PAGE_SIZE (max size this bpf hook can handle).
diff --git a/bpf/progs/netd.h b/bpf/progs/netd.h
index be7c311..8400679 100644
--- a/bpf/progs/netd.h
+++ b/bpf/progs/netd.h
@@ -185,6 +185,8 @@
#define PACKET_TRACE_RINGBUF_PATH BPF_NETD_PATH "map_netd_packet_trace_ringbuf"
#define PACKET_TRACE_ENABLED_MAP_PATH BPF_NETD_PATH "map_netd_packet_trace_enabled_map"
#define DATA_SAVER_ENABLED_MAP_PATH BPF_NETD_PATH "map_netd_data_saver_enabled_map"
+#define LOCAL_NET_ACCESS_MAP_PATH BPF_NETD_PATH "map_netd_local_net_access_map"
+#define LOCAL_NET_BLOCKED_UID_MAP_PATH BPF_NETD_PATH "map_netd_local_net_blocked_uid_map"
#endif // __cplusplus
@@ -245,6 +247,18 @@
} IngressDiscardValue;
STRUCT_SIZE(IngressDiscardValue, 2 * 4); // 8
+typedef struct {
+ // Longest prefix match length in bits (value from 0 to 192).
+ uint32_t lpm_bitlen;
+ uint32_t if_index;
+ // IPv4 uses IPv4-mapped IPv6 address format.
+ struct in6_addr remote_ip6;
+ // u16 instead of u8 to avoid padding due to alignment requirement.
+ uint16_t protocol;
+ __be16 remote_port;
+} LocalNetAccessKey;
+STRUCT_SIZE(LocalNetAccessKey, 4 + 4 + 16 + 2 + 2); // 28
+
// Entry in the configuration map that stores which UID rules are enabled.
#define UID_RULES_CONFIGURATION_KEY 0
// Entry in the configuration map that stores which stats map is currently in use.
diff --git a/bpf/progs/offload.c b/bpf/progs/offload.c
index 631908a..0f23844 100644
--- a/bpf/progs/offload.c
+++ b/bpf/progs/offload.c
@@ -609,27 +609,27 @@
// Full featured (required) implementations for 5.8+ kernels (these are S+ by definition)
DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_rawip$5_8", AID_ROOT, AID_NETWORK_STACK,
- sched_cls_tether_downstream4_rawip_5_8, KVER_5_8)
+ sched_cls_tether_downstream4_rawip_5_8, KVER_5_10)
(struct __sk_buff* skb) {
- return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER_5_8);
+ return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER_5_10);
}
DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_rawip$5_8", AID_ROOT, AID_NETWORK_STACK,
- sched_cls_tether_upstream4_rawip_5_8, KVER_5_8)
+ sched_cls_tether_upstream4_rawip_5_8, KVER_5_10)
(struct __sk_buff* skb) {
- return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER_5_8);
+ return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER_5_10);
}
DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_ether$5_8", AID_ROOT, AID_NETWORK_STACK,
- sched_cls_tether_downstream4_ether_5_8, KVER_5_8)
+ sched_cls_tether_downstream4_ether_5_8, KVER_5_10)
(struct __sk_buff* skb) {
- return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER_5_8);
+ return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER_5_10);
}
DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_ether$5_8", AID_ROOT, AID_NETWORK_STACK,
- sched_cls_tether_upstream4_ether_5_8, KVER_5_8)
+ sched_cls_tether_upstream4_ether_5_8, KVER_5_10)
(struct __sk_buff* skb) {
- return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER_5_8);
+ return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER_5_10);
}
// Full featured (optional) implementations for 4.14-S, 4.19-S & 5.4-S kernels
@@ -638,7 +638,7 @@
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$opt",
AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_downstream4_rawip_opt,
- KVER_4_14, KVER_5_8)
+ KVER_4_14, KVER_5_10)
(struct __sk_buff* skb) {
return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER_4_14);
}
@@ -646,7 +646,7 @@
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$opt",
AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_upstream4_rawip_opt,
- KVER_4_14, KVER_5_8)
+ KVER_4_14, KVER_5_10)
(struct __sk_buff* skb) {
return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER_4_14);
}
@@ -654,7 +654,7 @@
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$opt",
AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_downstream4_ether_opt,
- KVER_4_14, KVER_5_8)
+ KVER_4_14, KVER_5_10)
(struct __sk_buff* skb) {
return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER_4_14);
}
@@ -662,7 +662,7 @@
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$opt",
AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_upstream4_ether_opt,
- KVER_4_14, KVER_5_8)
+ KVER_4_14, KVER_5_10)
(struct __sk_buff* skb) {
return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER_4_14);
}
@@ -682,13 +682,13 @@
// RAWIP: Required for 5.4-R kernels -- which always support bpf_skb_change_head().
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$5_4", AID_ROOT, AID_NETWORK_STACK,
- sched_cls_tether_downstream4_rawip_5_4, KVER_5_4, KVER_5_8)
+ sched_cls_tether_downstream4_rawip_5_4, KVER_5_4, KVER_5_10)
(struct __sk_buff* skb) {
return do_forward4(skb, RAWIP, DOWNSTREAM, NO_UPDATETIME, KVER_5_4);
}
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$5_4", AID_ROOT, AID_NETWORK_STACK,
- sched_cls_tether_upstream4_rawip_5_4, KVER_5_4, KVER_5_8)
+ sched_cls_tether_upstream4_rawip_5_4, KVER_5_4, KVER_5_10)
(struct __sk_buff* skb) {
return do_forward4(skb, RAWIP, UPSTREAM, NO_UPDATETIME, KVER_5_4);
}
@@ -715,13 +715,13 @@
// ETHER: Required for 4.14-Q/R, 4.19-Q/R & 5.4-R kernels.
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$4_14", AID_ROOT, AID_NETWORK_STACK,
- sched_cls_tether_downstream4_ether_4_14, KVER_4_14, KVER_5_8)
+ sched_cls_tether_downstream4_ether_4_14, KVER_4_14, KVER_5_10)
(struct __sk_buff* skb) {
return do_forward4(skb, ETHER, DOWNSTREAM, NO_UPDATETIME, KVER_4_14);
}
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$4_14", AID_ROOT, AID_NETWORK_STACK,
- sched_cls_tether_upstream4_ether_4_14, KVER_4_14, KVER_5_8)
+ sched_cls_tether_upstream4_ether_4_14, KVER_4_14, KVER_5_10)
(struct __sk_buff* skb) {
return do_forward4(skb, ETHER, UPSTREAM, NO_UPDATETIME, KVER_4_14);
}
@@ -805,7 +805,7 @@
}
#define DEFINE_XDP_PROG(str, func) \
- DEFINE_BPF_PROG_KVER(str, AID_ROOT, AID_NETWORK_STACK, func, KVER_5_9)(struct xdp_md *ctx)
+ DEFINE_BPF_PROG_KVER(str, AID_ROOT, AID_NETWORK_STACK, func, KVER_5_10)(struct xdp_md *ctx)
DEFINE_XDP_PROG("xdp/tether_downstream_ether",
xdp_tether_downstream_ether) {
diff --git a/bpf/tests/mts/bpf_existence_test.cpp b/bpf/tests/mts/bpf_existence_test.cpp
index 0b5b7be..2cfa546 100644
--- a/bpf/tests/mts/bpf_existence_test.cpp
+++ b/bpf/tests/mts/bpf_existence_test.cpp
@@ -20,7 +20,9 @@
#include <set>
#include <string>
+#include <android-base/properties.h>
#include <android-modules-utils/sdk_level.h>
+#include <android/api-level.h>
#include <bpf/BpfUtils.h>
#include <gtest/gtest.h>
@@ -46,6 +48,11 @@
class BpfExistenceTest : public ::testing::Test {
};
+//ToDo: replace isAtLeast25Q2 with IsAtLeastB once sdk_level have been upgraded to 36 on aosp/main
+const bool unreleased = (android::base::GetProperty("ro.build.version.codename", "REL") != "REL");
+const int api_level = unreleased ? __ANDROID_API_FUTURE__ : android_get_device_api_level();
+const bool isAtLeast25Q2 = (api_level > __ANDROID_API_V__);
+
// Part of Android R platform (for 4.9+), but mainlined in S
static const set<string> PLATFORM_ONLY_IN_R = {
PLATFORM "map_offload_tether_ingress_map",
@@ -159,6 +166,12 @@
NETD "prog_netd_setsockopt_prog",
};
+// Provided by *current* mainline module for 25Q2+ devices
+static const set<string> MAINLINE_FOR_25Q2_PLUS = {
+ NETD "map_netd_local_net_access_map",
+ NETD "map_netd_local_net_blocked_uid_map",
+};
+
static void addAll(set<string>& a, const set<string>& b) {
a.insert(b.begin(), b.end());
}
@@ -209,6 +222,9 @@
DO_EXPECT(IsAtLeastV(), MAINLINE_FOR_V_PLUS);
DO_EXPECT(IsAtLeastV() && isAtLeastKernelVersion(5, 4, 0), MAINLINE_FOR_V_5_4_PLUS);
+ if (isAtLeast25Q2) ASSERT_TRUE(isAtLeastKernelVersion(5, 4, 0));
+ DO_EXPECT(isAtLeast25Q2, MAINLINE_FOR_25Q2_PLUS);
+
for (const auto& file : mustExist) {
EXPECT_EQ(0, access(file.c_str(), R_OK)) << file << " does not exist";
}
diff --git a/common/flags.aconfig b/common/flags.aconfig
index 60a827b..5f279fa 100644
--- a/common/flags.aconfig
+++ b/common/flags.aconfig
@@ -165,3 +165,12 @@
bug: "372936361"
is_fixed_read_only: true
}
+
+flag {
+ name: "restrict_local_network"
+ is_exported: true
+ namespace: "android_core_networking"
+ description: "Flag for controlling access to the local network behind a new runtime permission. Requires ConnectivityCompatChanges.RESTRICT_LOCAL_NETWORK to enable feature."
+ bug: "365139289"
+ is_fixed_read_only: true
+}
diff --git a/common/src/com/android/net/module/util/bpf/LocalNetAccessKey.java b/common/src/com/android/net/module/util/bpf/LocalNetAccessKey.java
new file mode 100644
index 0000000..95265b9
--- /dev/null
+++ b/common/src/com/android/net/module/util/bpf/LocalNetAccessKey.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.bpf;
+
+import com.android.net.module.util.InetAddressUtils;
+import com.android.net.module.util.Struct;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+
+public class LocalNetAccessKey extends Struct {
+
+ @Field(order = 0, type = Type.U32)
+ public final long lpmBitlen;
+ @Field(order = 1, type = Type.U32)
+ public final long ifIndex;
+ @Field(order = 2, type = Type.Ipv6Address)
+ public final Inet6Address remoteAddress;
+ @Field(order = 3, type = Type.U16)
+ public final int protocol;
+ @Field(order = 4, type = Type.UBE16)
+ public final int remotePort;
+
+ public LocalNetAccessKey(long lpmBitlen, long ifIndex, InetAddress remoteAddress, int protocol,
+ int remotePort) {
+ this.lpmBitlen = lpmBitlen;
+ this.ifIndex = ifIndex;
+ this.protocol = protocol;
+ this.remotePort = remotePort;
+
+ if (remoteAddress instanceof Inet4Address) {
+ this.remoteAddress = InetAddressUtils.v4MappedV6Address((Inet4Address) remoteAddress);
+ } else {
+ this.remoteAddress = (Inet6Address) remoteAddress;
+ }
+ }
+
+ public LocalNetAccessKey(long lpmBitlen, long ifIndex, Inet6Address remoteAddress, int protocol,
+ int remotePort) {
+ this.lpmBitlen = lpmBitlen;
+ this.ifIndex = ifIndex;
+ this.remoteAddress = remoteAddress;
+ this.protocol = protocol;
+ this.remotePort = remotePort;
+ }
+
+ @Override
+ public String toString() {
+ return "LocalNetAccessKey{"
+ + "lpmBitlen=" + lpmBitlen
+ + ", ifIndex=" + ifIndex
+ + ", remoteAddress=" + remoteAddress
+ + ", protocol=" + protocol
+ + ", remotePort=" + remotePort
+ + "}";
+ }
+}
diff --git a/framework-t/src/android/net/NetworkStatsAccess.java b/framework-t/src/android/net/NetworkStatsAccess.java
index 7c9b3ec..449588a 100644
--- a/framework-t/src/android/net/NetworkStatsAccess.java
+++ b/framework-t/src/android/net/NetworkStatsAccess.java
@@ -111,6 +111,12 @@
/** Returns the {@link NetworkStatsAccess.Level} for the given caller. */
public static @NetworkStatsAccess.Level int checkAccessLevel(
Context context, int callingPid, int callingUid, @Nullable String callingPackage) {
+ final int appId = UserHandle.getAppId(callingUid);
+ if (appId == Process.SYSTEM_UID) {
+ // the system can access data usage for all apps on the device.
+ // check system uid first, to avoid possible dead lock from other APIs
+ return NetworkStatsAccess.Level.DEVICE;
+ }
final DevicePolicyManager mDpm = context.getSystemService(DevicePolicyManager.class);
final TelephonyManager tm = (TelephonyManager)
context.getSystemService(Context.TELEPHONY_SERVICE);
@@ -126,16 +132,13 @@
Binder.restoreCallingIdentity(token);
}
- final int appId = UserHandle.getAppId(callingUid);
-
final boolean isNetworkStack = PermissionUtils.hasAnyPermissionOf(
context, callingPid, callingUid, android.Manifest.permission.NETWORK_STACK,
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
- if (hasCarrierPrivileges || isDeviceOwner
- || appId == Process.SYSTEM_UID || isNetworkStack) {
- // Carrier-privileged apps and device owners, and the system (including the
- // network stack) can access data usage for all apps on the device.
+ if (hasCarrierPrivileges || isDeviceOwner || isNetworkStack) {
+ // Carrier-privileged apps and device owners, and the network stack
+ // can access data usage for all apps on the device.
return NetworkStatsAccess.Level.DEVICE;
}
diff --git a/framework-t/src/android/net/nsd/OffloadServiceInfo.java b/framework-t/src/android/net/nsd/OffloadServiceInfo.java
index e4b2f43..fd824f3 100644
--- a/framework-t/src/android/net/nsd/OffloadServiceInfo.java
+++ b/framework-t/src/android/net/nsd/OffloadServiceInfo.java
@@ -282,7 +282,7 @@
}
/**
- * Get the service type. (e.g. "_http._tcp.local" )
+ * Get the service type. (e.g. "_http._tcp" )
*/
@NonNull
public String getServiceType() {
diff --git a/framework/api/current.txt b/framework/api/current.txt
index 797c107..323c533 100644
--- a/framework/api/current.txt
+++ b/framework/api/current.txt
@@ -233,6 +233,32 @@
field @NonNull public static final android.os.Parcelable.Creator<android.net.IpPrefix> CREATOR;
}
+ @FlaggedApi("com.android.net.flags.ipv6_over_ble") public final class L2capNetworkSpecifier extends android.net.NetworkSpecifier implements android.os.Parcelable {
+ method public int describeContents();
+ method public int getHeaderCompression();
+ method public int getPsm();
+ method @Nullable public android.net.MacAddress getRemoteAddress();
+ method public int getRole();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.net.L2capNetworkSpecifier> CREATOR;
+ field public static final int HEADER_COMPRESSION_6LOWPAN = 2; // 0x2
+ field public static final int HEADER_COMPRESSION_ANY = 0; // 0x0
+ field public static final int HEADER_COMPRESSION_NONE = 1; // 0x1
+ field public static final int PSM_ANY = 0; // 0x0
+ field public static final int ROLE_ANY = 0; // 0x0
+ field public static final int ROLE_CLIENT = 1; // 0x1
+ field public static final int ROLE_SERVER = 2; // 0x2
+ }
+
+ public static final class L2capNetworkSpecifier.Builder {
+ ctor public L2capNetworkSpecifier.Builder();
+ method @NonNull public android.net.L2capNetworkSpecifier build();
+ method @NonNull public android.net.L2capNetworkSpecifier.Builder setHeaderCompression(int);
+ method @NonNull public android.net.L2capNetworkSpecifier.Builder setPsm(@IntRange(from=0, to=255) int);
+ method @NonNull public android.net.L2capNetworkSpecifier.Builder setRemoteAddress(@Nullable android.net.MacAddress);
+ method @NonNull public android.net.L2capNetworkSpecifier.Builder setRole(int);
+ }
+
public class LinkAddress implements android.os.Parcelable {
method public int describeContents();
method public java.net.InetAddress getAddress();
diff --git a/framework/src/android/net/BpfNetMapsConstants.java b/framework/src/android/net/BpfNetMapsConstants.java
index f3773de..f1a6f00 100644
--- a/framework/src/android/net/BpfNetMapsConstants.java
+++ b/framework/src/android/net/BpfNetMapsConstants.java
@@ -60,6 +60,11 @@
"/sys/fs/bpf/netd_shared/map_netd_data_saver_enabled_map";
public static final String INGRESS_DISCARD_MAP_PATH =
"/sys/fs/bpf/netd_shared/map_netd_ingress_discard_map";
+ public static final String LOCAL_NET_ACCESS_MAP_PATH =
+ "/sys/fs/bpf/netd_shared/map_netd_local_net_access_map";
+ public static final String LOCAL_NET_BLOCKED_UID_MAP_PATH =
+ "/sys/fs/bpf/netd_shared/map_netd_local_net_blocked_uid_map";
+
public static final Struct.S32 UID_RULES_CONFIGURATION_KEY = new Struct.S32(0);
public static final Struct.S32 CURRENT_STATS_MAP_CONFIGURATION_KEY = new Struct.S32(1);
public static final Struct.S32 DATA_SAVER_ENABLED_KEY = new Struct.S32(0);
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 9016d13..5d99b74 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -3065,7 +3065,8 @@
* <p>WARNING: New clients should not use this function. The only usages should be in PanService
* and WifiStateMachine which need direct access. All other clients should use
* {@link #startTethering} and {@link #stopTethering} which encapsulate proper provisioning
- * logic.</p>
+ * logic. On SDK versions after {@link Build.VERSION_CODES.VANILLA_ICE_CREAM}, this will throw
+ * an UnsupportedOperationException.</p>
*
* @param iface the interface name to tether.
* @return error a {@code TETHER_ERROR} value indicating success or failure type
@@ -3090,7 +3091,8 @@
* <p>WARNING: New clients should not use this function. The only usages should be in PanService
* and WifiStateMachine which need direct access. All other clients should use
* {@link #startTethering} and {@link #stopTethering} which encapsulate proper provisioning
- * logic.</p>
+ * logic. On SDK versions after {@link Build.VERSION_CODES.VANILLA_ICE_CREAM}, this will throw
+ * an UnsupportedOperationException.</p>
*
* @param iface the interface name to untether.
* @return error a {@code TETHER_ERROR} value indicating success or failure type
diff --git a/framework/src/android/net/L2capNetworkSpecifier.java b/framework/src/android/net/L2capNetworkSpecifier.java
new file mode 100644
index 0000000..cfc9ed9
--- /dev/null
+++ b/framework/src/android/net/L2capNetworkSpecifier.java
@@ -0,0 +1,370 @@
+/*
+ * 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 android.net;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.net.flags.Flags;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * A {@link NetworkSpecifier} used to identify an L2CAP network over BLE.
+ *
+ * An L2CAP network is not symmetrical, meaning there exists both a server (Bluetooth peripheral)
+ * and a client (Bluetooth central) node. This specifier contains the information required to
+ * request a client L2CAP network using {@link ConnectivityManager#requestNetwork} while specifying
+ * the remote MAC address, and Protocol/Service Multiplexer (PSM). It can also contain information
+ * allocated by the system when reserving a server network using {@link
+ * ConnectivityManager#reserveNetwork} such as the Protocol/Service Multiplexer (PSM). In both
+ * cases, the header compression option must be specified.
+ *
+ * An L2CAP server network allocates a Protocol/Service Multiplexer (PSM) to be advertised to the
+ * client. A new server network must always be reserved using {@code
+ * ConnectivityManager#reserveNetwork}. The subsequent {@link
+ * ConnectivityManager.NetworkCallback#onReserved(NetworkCapabilities)} callback includes an {@code
+ * L2CapNetworkSpecifier}. The {@link getPsm()} method will return the Protocol/Service Multiplexer
+ * (PSM) of the reserved network so that the server can advertise it to the client and the client
+ * can connect.
+ * An L2CAP server network is backed by a {@link android.bluetooth.BluetoothServerSocket} which can,
+ * in theory, accept many connections. However, before SDK version {@link
+ * Build.VERSION_CODES.VANILLA_ICE_CREAM} Bluetooth APIs do not expose the channel ID, so these
+ * connections are indistinguishable. In practice, this means that the network matching semantics in
+ * ConnectivityService will tear down all but the first connection.
+ *
+ * When the connection between client and server completes, a {@link Network} whose capabilities
+ * satisfy this {@code L2capNetworkSpecifier} will connect and the usual callbacks, such as {@link
+ * NetworkCallback#onAvailable}, will be called on the callback object passed to {@code
+ * ConnectivityManager#reserveNetwork} or {@code ConnectivityManager#requestNetwork}.
+ */
+@FlaggedApi(Flags.FLAG_IPV6_OVER_BLE)
+public final class L2capNetworkSpecifier extends NetworkSpecifier implements Parcelable {
+ /**
+ * Match any role.
+ *
+ * This role is only meaningful in {@link NetworkRequest}s. Specifiers for actual L2CAP
+ * networks never have this role set.
+ */
+ public static final int ROLE_ANY = 0;
+ /** Specifier describes a client network, i.e., the device is the Bluetooth central. */
+ public static final int ROLE_CLIENT = 1;
+ /** Specifier describes a server network, i.e., the device is the Bluetooth peripheral. */
+ public static final int ROLE_SERVER = 2;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = false, prefix = "ROLE_", value = {
+ ROLE_ANY,
+ ROLE_CLIENT,
+ ROLE_SERVER
+ })
+ public @interface Role {}
+ /** Role used to distinguish client from server networks. */
+ @Role
+ private final int mRole;
+
+ /**
+ * Accept any form of header compression.
+ *
+ * This option is only meaningful in {@link NetworkRequest}s. Specifiers for actual L2CAP
+ * networks never have this option set.
+ */
+ public static final int HEADER_COMPRESSION_ANY = 0;
+ /** Do not compress packets on this network. */
+ public static final int HEADER_COMPRESSION_NONE = 1;
+ /** Use 6lowpan header compression as specified in rfc6282. */
+ public static final int HEADER_COMPRESSION_6LOWPAN = 2;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = false, prefix = "HEADER_COMPRESSION_", value = {
+ HEADER_COMPRESSION_ANY,
+ HEADER_COMPRESSION_NONE,
+ HEADER_COMPRESSION_6LOWPAN
+ })
+ public @interface HeaderCompression {}
+ /** Header compression mechanism used on this network. */
+ @HeaderCompression
+ private final int mHeaderCompression;
+
+ /** The MAC address of the remote. */
+ @Nullable
+ private final MacAddress mRemoteAddress;
+
+ /**
+ * Match any Protocol/Service Multiplexer (PSM).
+ *
+ * This PSM value is only meaningful in {@link NetworkRequest}s. Specifiers for actual L2CAP
+ * networks never have this value set.
+ */
+ public static final int PSM_ANY = 0;
+
+ /** The Bluetooth L2CAP Protocol/Service Multiplexer (PSM). */
+ private final int mPsm;
+
+ private L2capNetworkSpecifier(Parcel in) {
+ mRole = in.readInt();
+ mHeaderCompression = in.readInt();
+ mRemoteAddress = in.readParcelable(getClass().getClassLoader());
+ mPsm = in.readInt();
+ }
+
+ /** @hide */
+ public L2capNetworkSpecifier(@Role int role, @HeaderCompression int headerCompression,
+ MacAddress remoteAddress, int psm) {
+ mRole = role;
+ mHeaderCompression = headerCompression;
+ mRemoteAddress = remoteAddress;
+ mPsm = psm;
+ }
+
+ /** Returns the role to be used for this network. */
+ @Role
+ public int getRole() {
+ return mRole;
+ }
+
+ /** Returns the compression mechanism for this network. */
+ @HeaderCompression
+ public int getHeaderCompression() {
+ return mHeaderCompression;
+ }
+
+ /**
+ * Returns the remote MAC address for this network to connect to.
+ *
+ * The remote address is only meaningful for networks that have ROLE_CLIENT.
+ *
+ * When receiving this {@link L2capNetworkSpecifier} from Connectivity APIs such as a {@link
+ * ConnectivityManager.NetworkCallback}, the MAC address is redacted.
+ */
+ public @Nullable MacAddress getRemoteAddress() {
+ return mRemoteAddress;
+ }
+
+ /** Returns the Protocol/Service Multiplexer (PSM) for this network to connect to. */
+ public int getPsm() {
+ return mPsm;
+ }
+
+ /** A builder class for L2capNetworkSpecifier. */
+ public static final class Builder {
+ @Role
+ private int mRole = ROLE_ANY;
+ @HeaderCompression
+ private int mHeaderCompression = HEADER_COMPRESSION_ANY;
+ @Nullable
+ private MacAddress mRemoteAddress;
+ private int mPsm = PSM_ANY;
+
+ /**
+ * Set the role to use for this network.
+ *
+ * If not set, defaults to {@link ROLE_ANY}.
+ *
+ * @param role the role to use.
+ */
+ @NonNull
+ public Builder setRole(@Role int role) {
+ mRole = role;
+ return this;
+ }
+
+ /**
+ * Set the header compression mechanism to use for this network.
+ *
+ * If not set, defaults to {@link HEADER_COMPRESSION_ANY}. This option must be specified
+ * (i.e. must not be set to {@link HEADER_COMPRESSION_ANY}) when requesting or reserving a
+ * new network.
+ *
+ * @param headerCompression the header compression mechanism to use.
+ */
+ @NonNull
+ public Builder setHeaderCompression(@HeaderCompression int headerCompression) {
+ mHeaderCompression = headerCompression;
+ return this;
+ }
+
+ /**
+ * Set the remote address for the client to connect to.
+ *
+ * Only valid for client networks. If not set, the specifier matches any MAC address.
+ *
+ * @param remoteAddress the MAC address to connect to, or null to match any MAC address.
+ */
+ @NonNull
+ public Builder setRemoteAddress(@Nullable MacAddress remoteAddress) {
+ mRemoteAddress = remoteAddress;
+ return this;
+ }
+
+ /**
+ * Set the Protocol/Service Multiplexer (PSM) for the client to connect to.
+ *
+ * If not set, defaults to {@link PSM_ANY}.
+ *
+ * @param psm the Protocol/Service Multiplexer (PSM) to connect to.
+ */
+ @NonNull
+ public Builder setPsm(@IntRange(from = 0, to = 255) int psm) {
+ if (psm < 0 /* PSM_ANY */ || psm > 0xFF) {
+ throw new IllegalArgumentException("PSM must be PSM_ANY or within range [1, 255]");
+ }
+ mPsm = psm;
+ return this;
+ }
+
+ /** Create the L2capNetworkSpecifier object. */
+ @NonNull
+ public L2capNetworkSpecifier build() {
+ if (mRole == ROLE_SERVER && mRemoteAddress != null) {
+ throw new IllegalArgumentException(
+ "Specifying a remote address is not valid for server role.");
+ }
+ return new L2capNetworkSpecifier(mRole, mHeaderCompression, mRemoteAddress, mPsm);
+ }
+ }
+
+ /** @hide */
+ @Override
+ public boolean canBeSatisfiedBy(NetworkSpecifier other) {
+ if (!(other instanceof L2capNetworkSpecifier)) return false;
+ final L2capNetworkSpecifier rhs = (L2capNetworkSpecifier) other;
+
+ // A network / offer cannot be ROLE_ANY, but it is added for consistency.
+ if (mRole != rhs.mRole && mRole != ROLE_ANY && rhs.mRole != ROLE_ANY) {
+ return false;
+ }
+
+ if (mHeaderCompression != rhs.mHeaderCompression
+ && mHeaderCompression != HEADER_COMPRESSION_ANY
+ && rhs.mHeaderCompression != HEADER_COMPRESSION_ANY) {
+ return false;
+ }
+
+ if (!Objects.equals(mRemoteAddress, rhs.mRemoteAddress)
+ && mRemoteAddress != null && rhs.mRemoteAddress != null) {
+ return false;
+ }
+
+ if (mPsm != rhs.mPsm && mPsm != PSM_ANY && rhs.mPsm != PSM_ANY) {
+ return false;
+ }
+ return true;
+ }
+
+ /** @hide */
+ @Override
+ @Nullable
+ public NetworkSpecifier redact() {
+ final NetworkSpecifier redactedSpecifier = new Builder()
+ .setRole(mRole)
+ .setHeaderCompression(mHeaderCompression)
+ // The remote address is redacted.
+ .setRemoteAddress(null)
+ .setPsm(mPsm)
+ .build();
+ return redactedSpecifier;
+ }
+
+ /** @hide */
+ @Override
+ public int hashCode() {
+ return Objects.hash(mRole, mHeaderCompression, mRemoteAddress, mPsm);
+ }
+
+ /** @hide */
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (!(obj instanceof L2capNetworkSpecifier)) return false;
+
+ final L2capNetworkSpecifier rhs = (L2capNetworkSpecifier) obj;
+ return mRole == rhs.mRole
+ && mHeaderCompression == rhs.mHeaderCompression
+ && Objects.equals(mRemoteAddress, rhs.mRemoteAddress)
+ && mPsm == rhs.mPsm;
+ }
+
+ /** @hide */
+ @Override
+ public String toString() {
+ final String role;
+ switch (mRole) {
+ case ROLE_CLIENT:
+ role = "ROLE_CLIENT";
+ break;
+ case ROLE_SERVER:
+ role = "ROLE_SERVER";
+ break;
+ default:
+ role = "ROLE_ANY";
+ break;
+ }
+
+ final String headerCompression;
+ switch (mHeaderCompression) {
+ case HEADER_COMPRESSION_NONE:
+ headerCompression = "HEADER_COMPRESSION_NONE";
+ break;
+ case HEADER_COMPRESSION_6LOWPAN:
+ headerCompression = "HEADER_COMPRESSION_6LOWPAN";
+ break;
+ default:
+ headerCompression = "HEADER_COMPRESSION_ANY";
+ break;
+ }
+
+ final String psm = (mPsm == PSM_ANY) ? "PSM_ANY" : String.valueOf(mPsm);
+
+ return String.format("L2capNetworkSpecifier(%s, %s, RemoteAddress=%s, PSM=%s)",
+ role, headerCompression, Objects.toString(mRemoteAddress), psm);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mRole);
+ dest.writeInt(mHeaderCompression);
+ dest.writeParcelable(mRemoteAddress, flags);
+ dest.writeInt(mPsm);
+ }
+
+ public static final @NonNull Creator<L2capNetworkSpecifier> CREATOR = new Creator<>() {
+ @Override
+ public L2capNetworkSpecifier createFromParcel(Parcel in) {
+ return new L2capNetworkSpecifier(in);
+ }
+
+ @Override
+ public L2capNetworkSpecifier[] newArray(int size) {
+ return new L2capNetworkSpecifier[size];
+ }
+ };
+}
diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java
index b95363a..5a08d44 100644
--- a/framework/src/android/net/NetworkRequest.java
+++ b/framework/src/android/net/NetworkRequest.java
@@ -282,6 +282,13 @@
this.type = that.type;
}
+ private NetworkRequest(Parcel in) {
+ networkCapabilities = NetworkCapabilities.CREATOR.createFromParcel(in);
+ legacyType = in.readInt();
+ requestId = in.readInt();
+ type = Type.valueOf(in.readString()); // IllegalArgumentException if invalid.
+ }
+
/**
* Builder used to create {@link NetworkRequest} objects. Specify the Network features
* needed in terms of {@link NetworkCapabilities} features
@@ -678,12 +685,7 @@
public static final @android.annotation.NonNull Creator<NetworkRequest> CREATOR =
new Creator<NetworkRequest>() {
public NetworkRequest createFromParcel(Parcel in) {
- NetworkCapabilities nc = NetworkCapabilities.CREATOR.createFromParcel(in);
- int legacyType = in.readInt();
- int requestId = in.readInt();
- Type type = Type.valueOf(in.readString()); // IllegalArgumentException if invalid.
- NetworkRequest result = new NetworkRequest(nc, legacyType, requestId, type);
- return result;
+ return new NetworkRequest(in);
}
public NetworkRequest[] newArray(int size) {
return new NetworkRequest[size];
diff --git a/nearby/framework/java/android/nearby/NearbyManager.java b/nearby/framework/java/android/nearby/NearbyManager.java
index e78f999..9d7d144 100644
--- a/nearby/framework/java/android/nearby/NearbyManager.java
+++ b/nearby/framework/java/android/nearby/NearbyManager.java
@@ -46,6 +46,7 @@
import java.util.WeakHashMap;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
+import java.util.stream.Collectors;
/**
* This class provides a way to perform Nearby related operations such as scanning, broadcasting
@@ -503,7 +504,7 @@
PoweredOffFindingEphemeralId ephemeralId = new PoweredOffFindingEphemeralId();
ephemeralId.bytes = eid;
return ephemeralId;
- }).toList();
+ }).collect(Collectors.toUnmodifiableList());
try {
mService.setPoweredOffFindingEphemeralIds(ephemeralIdList);
} catch (RemoteException e) {
diff --git a/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
index 66ae79c..ac381b8 100644
--- a/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
@@ -89,6 +89,9 @@
break;
case BroadcastRequest.PRESENCE_VERSION_V1:
if (adapter.isLeExtendedAdvertisingSupported()) {
+ if (mAdvertisingSetCallback == null) {
+ mAdvertisingSetCallback = getAdvertisingSetCallback();
+ }
bluetoothLeAdvertiser.startAdvertisingSet(
getAdvertisingSetParameters(),
advertiseData,
@@ -133,6 +136,11 @@
}
mBroadcastListener = null;
mIsAdvertising = false;
+ // If called startAdvertisingSet() but onAdvertisingSetStopped() is not invoked yet,
+ // using the same mAdvertisingSetCallback will cause new advertising cann't be stopped.
+ // Therefore, release the old mAdvertisingSetCallback and
+ // create a new mAdvertisingSetCallback when calling startAdvertisingSet.
+ mAdvertisingSetCallback = null;
}
}
diff --git a/networksecurity/TEST_MAPPING b/networksecurity/TEST_MAPPING
index 20ecbce..448ee84 100644
--- a/networksecurity/TEST_MAPPING
+++ b/networksecurity/TEST_MAPPING
@@ -1,5 +1,17 @@
{
- "postsubmit": [
+ "presubmit": [
+ {
+ "name": "CtsNetSecConfigCertificateTransparencyTestCases"
+ },
+ {
+ "name": "CtsNetSecConfigCertificateTransparencyDefaultTestCases"
+ },
+ {
+ "name": "NetSecConfigCertificateTransparencySctLogListTestCases"
+ },
+ {
+ "name": "NetSecConfigCertificateTransparencySctNoLogListTestCases"
+ },
{
"name": "NetworkSecurityUnitTests"
}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
index 79123ee..1fbb3f3 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
@@ -16,37 +16,26 @@
package com.android.server.net.ct;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_DEVICE_OFFLINE;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_DOWNLOAD_CANNOT_RESUME;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_HTTP_ERROR;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_NO_DISK_SPACE;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_UNKNOWN;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_TOO_MANY_REDIRECTS;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_VERSION_ALREADY_EXISTS;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__PENDING_WAITING_FOR_WIFI;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
import android.annotation.RequiresApi;
import android.app.DownloadManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
-import android.content.IntentFilter;
import android.net.Uri;
import android.os.Build;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
+import com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState;
import com.android.server.net.ct.DownloadHelper.DownloadStatus;
-import org.json.JSONException;
-import org.json.JSONObject;
-
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
/** Helper class to download certificate transparency log files. */
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@@ -58,38 +47,33 @@
private final DataStore mDataStore;
private final DownloadHelper mDownloadHelper;
private final SignatureVerifier mSignatureVerifier;
- private final CertificateTransparencyInstaller mInstaller;
private final CertificateTransparencyLogger mLogger;
+ private final List<CompatibilityVersion> mCompatVersions = new ArrayList<>();
+
CertificateTransparencyDownloader(
Context context,
DataStore dataStore,
DownloadHelper downloadHelper,
SignatureVerifier signatureVerifier,
- CertificateTransparencyInstaller installer,
CertificateTransparencyLogger logger) {
mContext = context;
mSignatureVerifier = signatureVerifier;
mDataStore = dataStore;
mDownloadHelper = downloadHelper;
- mInstaller = installer;
mLogger = logger;
}
- void initialize() {
- mInstaller.addCompatibilityVersion(Config.COMPATIBILITY_VERSION);
+ void addCompatibilityVersion(CompatibilityVersion compatVersion) {
+ mCompatVersions.add(compatVersion);
+ }
- IntentFilter intentFilter = new IntentFilter();
- intentFilter.addAction(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
- mContext.registerReceiver(this, intentFilter, Context.RECEIVER_EXPORTED);
-
- if (Config.DEBUG) {
- Log.d(TAG, "CertificateTransparencyDownloader initialized successfully");
- }
+ void clearCompatibilityVersions() {
+ mCompatVersions.clear();
}
long startPublicKeyDownload() {
- long downloadId = download(mDataStore.getProperty(Config.PUBLIC_KEY_URL));
+ long downloadId = download(Config.URL_PUBLIC_KEY);
if (downloadId != -1) {
mDataStore.setPropertyLong(Config.PUBLIC_KEY_DOWNLOAD_ID, downloadId);
mDataStore.store();
@@ -97,19 +81,31 @@
return downloadId;
}
- long startMetadataDownload() {
- long downloadId = download(mDataStore.getProperty(Config.METADATA_URL));
+ private long startMetadataDownload(CompatibilityVersion compatVersion) {
+ long downloadId = download(compatVersion.getMetadataUrl());
if (downloadId != -1) {
- mDataStore.setPropertyLong(Config.METADATA_DOWNLOAD_ID, downloadId);
+ mDataStore.setPropertyLong(compatVersion.getMetadataPropertyName(), downloadId);
mDataStore.store();
}
return downloadId;
}
- long startContentDownload() {
- long downloadId = download(mDataStore.getProperty(Config.CONTENT_URL));
+ @VisibleForTesting
+ void startMetadataDownload() {
+ for (CompatibilityVersion compatVersion : mCompatVersions) {
+ if (startMetadataDownload(compatVersion) == -1) {
+ Log.e(TAG, "Metadata download not started for " + compatVersion.getCompatVersion());
+ } else if (Config.DEBUG) {
+ Log.d(TAG, "Metadata download started for " + compatVersion.getCompatVersion());
+ }
+ }
+ }
+
+ @VisibleForTesting
+ long startContentDownload(CompatibilityVersion compatVersion) {
+ long downloadId = download(compatVersion.getContentUrl());
if (downloadId != -1) {
- mDataStore.setPropertyLong(Config.CONTENT_DOWNLOAD_ID, downloadId);
+ mDataStore.setPropertyLong(compatVersion.getContentPropertyName(), downloadId);
mDataStore.store();
}
return downloadId;
@@ -123,25 +119,28 @@
return;
}
- long completedId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
+ long completedId =
+ intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, /* defaultValue= */ -1);
if (completedId == -1) {
Log.e(TAG, "Invalid completed download Id");
return;
}
- if (isPublicKeyDownloadId(completedId)) {
+ if (getPublicKeyDownloadId() == completedId) {
handlePublicKeyDownloadCompleted(completedId);
return;
}
- if (isMetadataDownloadId(completedId)) {
- handleMetadataDownloadCompleted(completedId);
- return;
- }
+ for (CompatibilityVersion compatVersion : mCompatVersions) {
+ if (getMetadataDownloadId(compatVersion) == completedId) {
+ handleMetadataDownloadCompleted(compatVersion, completedId);
+ return;
+ }
- if (isContentDownloadId(completedId)) {
- handleContentDownloadCompleted(completedId);
- return;
+ if (getContentDownloadId(compatVersion) == completedId) {
+ handleContentDownloadCompleted(compatVersion, completedId);
+ return;
+ }
}
Log.i(TAG, "Download id " + completedId + " is not recognized.");
@@ -167,152 +166,85 @@
return;
}
- if (startMetadataDownload() == -1) {
- Log.e(TAG, "Metadata download not started.");
- } else if (Config.DEBUG) {
- Log.d(TAG, "Metadata download started successfully.");
- }
+ startMetadataDownload();
}
- private void handleMetadataDownloadCompleted(long downloadId) {
+ private void handleMetadataDownloadCompleted(
+ CompatibilityVersion compatVersion, long downloadId) {
DownloadStatus status = mDownloadHelper.getDownloadStatus(downloadId);
if (!status.isSuccessful()) {
handleDownloadFailed(status);
return;
}
- if (startContentDownload() == -1) {
- Log.e(TAG, "Content download not started.");
+ if (startContentDownload(compatVersion) == -1) {
+ Log.e(TAG, "Content download failed for" + compatVersion.getCompatVersion());
} else if (Config.DEBUG) {
- Log.d(TAG, "Content download started successfully.");
+ Log.d(TAG, "Content download started for" + compatVersion.getCompatVersion());
}
}
- private void handleContentDownloadCompleted(long downloadId) {
+ private void handleContentDownloadCompleted(
+ CompatibilityVersion compatVersion, long downloadId) {
DownloadStatus status = mDownloadHelper.getDownloadStatus(downloadId);
if (!status.isSuccessful()) {
handleDownloadFailed(status);
return;
}
- Uri contentUri = getContentDownloadUri();
- Uri metadataUri = getMetadataDownloadUri();
+ Uri contentUri = getContentDownloadUri(compatVersion);
+ Uri metadataUri = getMetadataDownloadUri(compatVersion);
if (contentUri == null || metadataUri == null) {
Log.e(TAG, "Invalid URIs");
return;
}
- boolean success = false;
- try {
- success = mSignatureVerifier.verify(contentUri, metadataUri);
- } catch (IOException | GeneralSecurityException e) {
- Log.e(TAG, "Could not verify new log list", e);
- }
- if (!success) {
+ LogListUpdateStatus updateStatus = mSignatureVerifier.verify(contentUri, metadataUri);
+ // TODO(b/391327942): parse file and log the timestamp of the log list
+
+ if (!updateStatus.isSignatureVerified()) {
Log.w(TAG, "Log list did not pass verification");
- // TODO(b/384931263): add logging for failed signature verification
+ mLogger.logCTLogListUpdateStateChangedEvent(updateStatus);
+
return;
}
- String version = null;
- try (InputStream inputStream = mContext.getContentResolver().openInputStream(contentUri)) {
- version = new JSONObject(new String(inputStream.readAllBytes(), UTF_8))
- .getString("version");
- } catch (JSONException | IOException e) {
- Log.e(TAG, "Could not extract version from log list", e);
- return;
- }
+ boolean success = false;
try (InputStream inputStream = mContext.getContentResolver().openInputStream(contentUri)) {
- success = mInstaller.install(Config.COMPATIBILITY_VERSION, inputStream, version);
+ success = compatVersion.install(inputStream);
} catch (IOException e) {
Log.e(TAG, "Could not install new content", e);
return;
}
if (success) {
- // Update information about the stored version on successful install.
- mDataStore.setProperty(Config.VERSION, version);
-
// Reset the number of consecutive log list failure updates back to zero.
mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* value= */ 0);
mDataStore.store();
} else {
- if (updateFailureCount()) {
- mLogger.logCTLogListUpdateFailedEvent(
- CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_VERSION_ALREADY_EXISTS,
- mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0));
+ mLogger.logCTLogListUpdateStateChangedEvent(
+ updateStatus
+ .toBuilder()
+ .setState(CTLogListUpdateState.VERSION_ALREADY_EXISTS)
+ .build());
}
}
- }
private void handleDownloadFailed(DownloadStatus status) {
Log.e(TAG, "Download failed with " + status);
- if (updateFailureCount()) {
- int failureCount = mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0);
-
- // HTTP Error
- if (400 <= status.reason() && status.reason() <= 600) {
- mLogger.logCTLogListUpdateFailedEvent(
- CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_HTTP_ERROR,
- failureCount,
- status.reason()
- );
- } else {
- // TODO(b/384935059): handle blocked domain logging
- // TODO(b/384936292): add additionalchecks for pending wifi status
- mLogger.logCTLogListUpdateFailedEvent(
- downloadStatusToFailureReason(status.reason()),
- failureCount
- );
- }
+ LogListUpdateStatus.Builder updateStatus = LogListUpdateStatus.builder();
+ if (status.isHttpError()) {
+ updateStatus
+ .setState(CTLogListUpdateState.HTTP_ERROR)
+ .setHttpErrorStatusCode(status.reason());
+ } else {
+ // TODO(b/384935059): handle blocked domain logging
+ updateStatus.setDownloadStatus(Optional.of(status.reason()));
}
- }
- /** Converts DownloadStatus reason into failure reason to log. */
- private int downloadStatusToFailureReason(int downloadStatusReason) {
- switch(downloadStatusReason) {
- case DownloadManager.PAUSED_WAITING_TO_RETRY:
- case DownloadManager.PAUSED_WAITING_FOR_NETWORK:
- return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_DEVICE_OFFLINE;
- case DownloadManager.ERROR_UNHANDLED_HTTP_CODE:
- case DownloadManager.ERROR_HTTP_DATA_ERROR:
- return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_HTTP_ERROR;
- case DownloadManager.ERROR_TOO_MANY_REDIRECTS:
- return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_TOO_MANY_REDIRECTS;
- case DownloadManager.ERROR_CANNOT_RESUME:
- return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_DOWNLOAD_CANNOT_RESUME;
- case DownloadManager.ERROR_INSUFFICIENT_SPACE:
- return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_NO_DISK_SPACE;
- case DownloadManager.PAUSED_QUEUED_FOR_WIFI:
- return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__PENDING_WAITING_FOR_WIFI;
- default:
- return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_UNKNOWN;
- }
- }
-
- /**
- * Updates the data store with the current number of consecutive log list update failures.
- *
- * @return whether the failure count exceeds the threshold and should be logged.
- */
- private boolean updateFailureCount() {
- int failure_count = mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0);
- int new_failure_count = failure_count + 1;
-
- mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, new_failure_count);
- mDataStore.store();
-
- boolean shouldReport = new_failure_count >= Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD;
- if (shouldReport) {
- Log.d(TAG,
- "Log list update failure count exceeds threshold: " + new_failure_count);
- }
- return shouldReport;
+ mLogger.logCTLogListUpdateStateChangedEvent(updateStatus.build());
}
private long download(String url) {
@@ -326,17 +258,19 @@
@VisibleForTesting
long getPublicKeyDownloadId() {
- return mDataStore.getPropertyLong(Config.PUBLIC_KEY_DOWNLOAD_ID, -1);
+ return mDataStore.getPropertyLong(Config.PUBLIC_KEY_DOWNLOAD_ID, /* defaultValue= */ -1);
}
@VisibleForTesting
- long getMetadataDownloadId() {
- return mDataStore.getPropertyLong(Config.METADATA_DOWNLOAD_ID, -1);
+ long getMetadataDownloadId(CompatibilityVersion compatVersion) {
+ return mDataStore.getPropertyLong(
+ compatVersion.getMetadataPropertyName(), /* defaultValue */ -1);
}
@VisibleForTesting
- long getContentDownloadId() {
- return mDataStore.getPropertyLong(Config.CONTENT_DOWNLOAD_ID, -1);
+ long getContentDownloadId(CompatibilityVersion compatVersion) {
+ return mDataStore.getPropertyLong(
+ compatVersion.getContentPropertyName(), /* defaultValue= */ -1);
}
@VisibleForTesting
@@ -346,38 +280,27 @@
@VisibleForTesting
boolean hasMetadataDownloadId() {
- return getMetadataDownloadId() != -1;
+ return mCompatVersions.stream()
+ .map(this::getMetadataDownloadId)
+ .anyMatch(downloadId -> downloadId != -1);
}
@VisibleForTesting
boolean hasContentDownloadId() {
- return getContentDownloadId() != -1;
- }
-
- @VisibleForTesting
- boolean isPublicKeyDownloadId(long downloadId) {
- return getPublicKeyDownloadId() == downloadId;
- }
-
- @VisibleForTesting
- boolean isMetadataDownloadId(long downloadId) {
- return getMetadataDownloadId() == downloadId;
- }
-
- @VisibleForTesting
- boolean isContentDownloadId(long downloadId) {
- return getContentDownloadId() == downloadId;
+ return mCompatVersions.stream()
+ .map(this::getContentDownloadId)
+ .anyMatch(downloadId -> downloadId != -1);
}
private Uri getPublicKeyDownloadUri() {
return mDownloadHelper.getUri(getPublicKeyDownloadId());
}
- private Uri getMetadataDownloadUri() {
- return mDownloadHelper.getUri(getMetadataDownloadId());
+ private Uri getMetadataDownloadUri(CompatibilityVersion compatVersion) {
+ return mDownloadHelper.getUri(getMetadataDownloadId(compatVersion));
}
- private Uri getContentDownloadUri() {
- return mDownloadHelper.getUri(getContentDownloadId());
+ private Uri getContentDownloadUri(CompatibilityVersion compatVersion) {
+ return mDownloadHelper.getUri(getContentDownloadId(compatVersion));
}
}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
deleted file mode 100644
index 3138ea7..0000000
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.server.net.ct;
-
-import android.annotation.RequiresApi;
-import android.os.Build;
-import android.provider.DeviceConfig;
-import android.provider.DeviceConfig.Properties;
-import android.text.TextUtils;
-import android.util.Log;
-
-import java.security.GeneralSecurityException;
-import java.util.concurrent.Executors;
-
-/** Listener class for the Certificate Transparency Phenotype flags. */
-@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
-class CertificateTransparencyFlagsListener implements DeviceConfig.OnPropertiesChangedListener {
-
- private static final String TAG = "CertificateTransparencyFlagsListener";
-
- private final DataStore mDataStore;
- private final SignatureVerifier mSignatureVerifier;
- private final CertificateTransparencyDownloader mCertificateTransparencyDownloader;
-
- CertificateTransparencyFlagsListener(
- DataStore dataStore,
- SignatureVerifier signatureVerifier,
- CertificateTransparencyDownloader certificateTransparencyDownloader) {
- mDataStore = dataStore;
- mSignatureVerifier = signatureVerifier;
- mCertificateTransparencyDownloader = certificateTransparencyDownloader;
- }
-
- void initialize() {
- mDataStore.load();
- mCertificateTransparencyDownloader.initialize();
- DeviceConfig.addOnPropertiesChangedListener(
- Config.NAMESPACE_NETWORK_SECURITY, Executors.newSingleThreadExecutor(), this);
- if (Config.DEBUG) {
- Log.d(TAG, "CertificateTransparencyFlagsListener initialized successfully");
- }
- // TODO: handle property changes triggering on boot before registering this listener.
- }
-
- @Override
- public void onPropertiesChanged(Properties properties) {
- if (!Config.NAMESPACE_NETWORK_SECURITY.equals(properties.getNamespace())) {
- return;
- }
-
- String newPublicKey =
- DeviceConfig.getString(
- Config.NAMESPACE_NETWORK_SECURITY,
- Config.FLAG_PUBLIC_KEY,
- /* defaultValue= */ "");
- String newVersion =
- DeviceConfig.getString(
- Config.NAMESPACE_NETWORK_SECURITY,
- Config.FLAG_VERSION,
- /* defaultValue= */ "");
- String newContentUrl =
- DeviceConfig.getString(
- Config.NAMESPACE_NETWORK_SECURITY,
- Config.FLAG_CONTENT_URL,
- /* defaultValue= */ "");
- String newMetadataUrl =
- DeviceConfig.getString(
- Config.NAMESPACE_NETWORK_SECURITY,
- Config.FLAG_METADATA_URL,
- /* defaultValue= */ "");
- if (TextUtils.isEmpty(newPublicKey)
- || TextUtils.isEmpty(newVersion)
- || TextUtils.isEmpty(newContentUrl)
- || TextUtils.isEmpty(newMetadataUrl)) {
- return;
- }
-
- if (Config.DEBUG) {
- Log.d(TAG, "newPublicKey=" + newPublicKey);
- Log.d(TAG, "newVersion=" + newVersion);
- Log.d(TAG, "newContentUrl=" + newContentUrl);
- Log.d(TAG, "newMetadataUrl=" + newMetadataUrl);
- }
-
- String oldVersion = mDataStore.getProperty(Config.VERSION);
- String oldContentUrl = mDataStore.getProperty(Config.CONTENT_URL);
- String oldMetadataUrl = mDataStore.getProperty(Config.METADATA_URL);
-
- if (TextUtils.equals(newVersion, oldVersion)
- && TextUtils.equals(newContentUrl, oldContentUrl)
- && TextUtils.equals(newMetadataUrl, oldMetadataUrl)) {
- Log.i(TAG, "No flag changed, ignoring update");
- return;
- }
-
- try {
- mSignatureVerifier.setPublicKey(newPublicKey);
- } catch (GeneralSecurityException | IllegalArgumentException e) {
- Log.e(TAG, "Error setting the public Key", e);
- return;
- }
-
- // TODO: handle the case where there is already a pending download.
-
- mDataStore.setProperty(Config.CONTENT_URL, newContentUrl);
- mDataStore.setProperty(Config.METADATA_URL, newMetadataUrl);
- mDataStore.store();
-
- if (mCertificateTransparencyDownloader.startMetadataDownload() == -1) {
- Log.e(TAG, "Metadata download not started.");
- } else if (Config.DEBUG) {
- Log.d(TAG, "Metadata download started successfully.");
- }
- }
-}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java
deleted file mode 100644
index 9970667..0000000
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.server.net.ct;
-
-import android.util.Log;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.HashMap;
-import java.util.Map;
-
-/** Installer of CT log lists. */
-public class CertificateTransparencyInstaller {
-
- private static final String TAG = "CertificateTransparencyInstaller";
-
- private final Map<String, CompatibilityVersion> mCompatVersions = new HashMap<>();
-
- // The CT root directory.
- private final File mRootDirectory;
-
- public CertificateTransparencyInstaller(File rootDirectory) {
- mRootDirectory = rootDirectory;
- }
-
- public CertificateTransparencyInstaller(String rootDirectoryPath) {
- this(new File(rootDirectoryPath));
- }
-
- public CertificateTransparencyInstaller() {
- this(Config.CT_ROOT_DIRECTORY_PATH);
- }
-
- void addCompatibilityVersion(String versionName) {
- removeCompatibilityVersion(versionName);
- CompatibilityVersion newCompatVersion =
- new CompatibilityVersion(new File(mRootDirectory, versionName));
- mCompatVersions.put(versionName, newCompatVersion);
- }
-
- void removeCompatibilityVersion(String versionName) {
- CompatibilityVersion compatVersion = mCompatVersions.remove(versionName);
- if (compatVersion != null && !compatVersion.delete()) {
- Log.w(TAG, "Could not delete compatibility version directory.");
- }
- }
-
- CompatibilityVersion getCompatibilityVersion(String versionName) {
- return mCompatVersions.get(versionName);
- }
-
- /**
- * Install a new log list to use during SCT verification.
- *
- * @param compatibilityVersion the compatibility version of the new log list
- * @param newContent an input stream providing the log list
- * @param version the minor version of the new log list
- * @return true if the log list was installed successfully, false otherwise.
- * @throws IOException if the list cannot be saved in the CT directory.
- */
- public boolean install(String compatibilityVersion, InputStream newContent, String version)
- throws IOException {
- CompatibilityVersion compatVersion = mCompatVersions.get(compatibilityVersion);
- if (compatVersion == null) {
- Log.e(TAG, "No compatibility version for " + compatibilityVersion);
- return false;
- }
- // Ensure root directory exists and is readable.
- DirectoryUtils.makeDir(mRootDirectory);
-
- if (!compatVersion.install(newContent, version)) {
- Log.e(TAG, "Failed to install logs version " + version);
- return false;
- }
- Log.i(TAG, "New logs installed at " + compatVersion.getLogsDir());
- return true;
- }
-}
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 abede87..e6f1379 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
@@ -17,6 +17,7 @@
import android.annotation.RequiresApi;
import android.app.AlarmManager;
+import android.app.DownloadManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -36,38 +37,69 @@
private final Context mContext;
private final DataStore mDataStore;
private final CertificateTransparencyDownloader mCertificateTransparencyDownloader;
+ private final CompatibilityVersion mCompatVersion;
private final AlarmManager mAlarmManager;
+ private final PendingIntent mPendingIntent;
+ private boolean mScheduled = false;
private boolean mDependenciesReady = false;
/** Creates a new {@link CertificateTransparencyJob} object. */
public CertificateTransparencyJob(
Context context,
DataStore dataStore,
- CertificateTransparencyDownloader certificateTransparencyDownloader) {
+ CertificateTransparencyDownloader certificateTransparencyDownloader,
+ CompatibilityVersion compatVersion) {
mContext = context;
mDataStore = dataStore;
mCertificateTransparencyDownloader = certificateTransparencyDownloader;
- mAlarmManager = context.getSystemService(AlarmManager.class);
- }
+ mCompatVersion = compatVersion;
- void initialize() {
- mContext.registerReceiver(
- this,
- new IntentFilter(ConfigUpdate.ACTION_UPDATE_CT_LOGS),
- Context.RECEIVER_EXPORTED);
- mAlarmManager.setInexactRepeating(
- AlarmManager.ELAPSED_REALTIME,
- SystemClock.elapsedRealtime(), // schedule first job at earliest convenient time.
- AlarmManager.INTERVAL_DAY,
+ mAlarmManager = context.getSystemService(AlarmManager.class);
+ mPendingIntent =
PendingIntent.getBroadcast(
mContext,
- 0,
+ /* requestCode= */ 0,
new Intent(ConfigUpdate.ACTION_UPDATE_CT_LOGS),
- PendingIntent.FLAG_IMMUTABLE));
+ PendingIntent.FLAG_IMMUTABLE);
+ }
+
+ void schedule() {
+ if (!mScheduled) {
+ mContext.registerReceiver(
+ this,
+ new IntentFilter(ConfigUpdate.ACTION_UPDATE_CT_LOGS),
+ Context.RECEIVER_EXPORTED);
+ mAlarmManager.setInexactRepeating(
+ AlarmManager.ELAPSED_REALTIME,
+ SystemClock
+ .elapsedRealtime(), // schedule first job at earliest convenient time.
+ AlarmManager.INTERVAL_DAY,
+ mPendingIntent);
+ }
+ mScheduled = true;
if (Config.DEBUG) {
- Log.d(TAG, "CertificateTransparencyJob scheduled successfully.");
+ Log.d(TAG, "CertificateTransparencyJob scheduled.");
+ }
+ }
+
+ void cancel() {
+ if (mScheduled) {
+ mContext.unregisterReceiver(this);
+ mAlarmManager.cancel(mPendingIntent);
+ }
+ mScheduled = false;
+
+ if (mDependenciesReady) {
+ stopDependencies();
+ }
+ mDependenciesReady = false;
+
+ mCompatVersion.delete();
+
+ if (Config.DEBUG) {
+ Log.d(TAG, "CertificateTransparencyJob canceled.");
}
}
@@ -81,20 +113,37 @@
Log.d(TAG, "Starting CT daily job.");
}
if (!mDependenciesReady) {
- mDataStore.load();
- mCertificateTransparencyDownloader.initialize();
+ startDependencies();
mDependenciesReady = true;
}
- mDataStore.setProperty(Config.CONTENT_URL, Config.URL_LOG_LIST);
- mDataStore.setProperty(Config.METADATA_URL, Config.URL_SIGNATURE);
- mDataStore.setProperty(Config.PUBLIC_KEY_URL, Config.URL_PUBLIC_KEY);
- mDataStore.store();
-
if (mCertificateTransparencyDownloader.startPublicKeyDownload() == -1) {
Log.e(TAG, "Public key download not started.");
} else if (Config.DEBUG) {
Log.d(TAG, "Public key download started successfully.");
}
}
+
+ private void startDependencies() {
+ mDataStore.load();
+ mCertificateTransparencyDownloader.addCompatibilityVersion(mCompatVersion);
+ mContext.registerReceiver(
+ mCertificateTransparencyDownloader,
+ new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
+ Context.RECEIVER_EXPORTED);
+
+ if (Config.DEBUG) {
+ Log.d(TAG, "CertificateTransparencyJob dependencies ready.");
+ }
+ }
+
+ private void stopDependencies() {
+ mContext.unregisterReceiver(mCertificateTransparencyDownloader);
+ mCertificateTransparencyDownloader.clearCompatibilityVersions();
+ mDataStore.delete();
+
+ if (Config.DEBUG) {
+ Log.d(TAG, "CertificateTransparencyJob dependencies stopped.");
+ }
+ }
}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java
index 93493c2..967a04b 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java
@@ -16,37 +16,29 @@
package com.android.server.net.ct;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED;
-
-/** Helper class to interface with logging to statsd. */
-public class CertificateTransparencyLogger {
-
- public CertificateTransparencyLogger() {}
+/** Interface with logging to statsd for Certificate Transparency. */
+public interface CertificateTransparencyLogger {
/**
- * Logs a CTLogListUpdateFailed event to statsd, when no HTTP error status code is present.
+ * Logs a CTLogListUpdateStateChanged event to statsd.
*
- * @param failureReason reason why the log list wasn't updated (e.g. DownloadManager failures)
- * @param failureCount number of consecutive log list update failures
+ * @param updateStatus status object containing details from this update event (e.g. log list
+ * signature, log list timestamp, failure reason if applicable)
*/
- public void logCTLogListUpdateFailedEvent(int failureReason, int failureCount) {
- logCTLogListUpdateFailedEvent(failureReason, failureCount, /* httpErrorStatusCode= */ 0);
- }
+ void logCTLogListUpdateStateChangedEvent(LogListUpdateStatus updateStatus);
/**
- * Logs a CTLogListUpdateFailed event to statsd, when an HTTP error status code is provided.
+ * Intermediate enum for use with CertificateTransparencyStatsLog.
*
- * @param failureReason reason why the log list wasn't updated (e.g. DownloadManager failures)
- * @param failureCount number of consecutive log list update failures
- * @param httpErrorStatusCode if relevant, the HTTP error status code from DownloadManager
+ * This enum primarily exists to avoid 100+ char line alert fatigue.
*/
- public void logCTLogListUpdateFailedEvent(
- int failureReason, int failureCount, int httpErrorStatusCode) {
- CertificateTransparencyStatsLog.write(
- CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED,
- failureReason,
- failureCount,
- httpErrorStatusCode
- );
+ enum CTLogListUpdateState {
+ UNKNOWN_STATE,
+ HTTP_ERROR,
+ PUBLIC_KEY_NOT_FOUND,
+ SIGNATURE_INVALID,
+ SIGNATURE_NOT_FOUND,
+ SIGNATURE_VERIFICATION_FAILED,
+ VERSION_ALREADY_EXISTS
}
-}
+}
\ No newline at end of file
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java
new file mode 100644
index 0000000..9c3210d
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net.ct;
+
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_DEVICE_OFFLINE;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_DOWNLOAD_CANNOT_RESUME;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_HTTP_ERROR;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_NO_DISK_SPACE;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_PUBLIC_KEY_NOT_FOUND;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_INVALID;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_NOT_FOUND;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_VERIFICATION;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_TOO_MANY_REDIRECTS;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_UNKNOWN;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_VERSION_ALREADY_EXISTS;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__PENDING_WAITING_FOR_WIFI;
+
+import android.app.DownloadManager;
+
+/** Implementation for logging to statsd for Certificate Transparency. */
+class CertificateTransparencyLoggerImpl implements CertificateTransparencyLogger {
+
+ private final DataStore mDataStore;
+
+ CertificateTransparencyLoggerImpl(DataStore dataStore) {
+ mDataStore = dataStore;
+ }
+
+ @Override
+ public void logCTLogListUpdateStateChangedEvent(LogListUpdateStatus updateStatus) {
+ int updateState =
+ updateStatus
+ .downloadStatus()
+ .map(s -> downloadStatusToFailureReason(s))
+ .orElseGet(() -> localEnumToStatsLogEnum(updateStatus.state()));
+ int failureCount =
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0);
+
+ logCTLogListUpdateStateChangedEvent(
+ updateState,
+ failureCount,
+ updateStatus.httpErrorStatusCode(),
+ updateStatus.signature());
+ }
+
+ private void logCTLogListUpdateStateChangedEvent(
+ int updateState, int failureCount, int httpErrorStatusCode, String signature) {
+ updateFailureCount();
+
+ CertificateTransparencyStatsLog.write(
+ CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED,
+ updateState,
+ failureCount,
+ httpErrorStatusCode,
+ signature,
+ /* logListTimestampMs= */ 0);
+ }
+
+ /**
+ * Updates the data store with the current number of consecutive log list update failures.
+ */
+ private void updateFailureCount() {
+ int failure_count =
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0);
+ int new_failure_count = failure_count + 1;
+
+ mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, new_failure_count);
+ mDataStore.store();
+ }
+
+ /** Converts DownloadStatus reason into failure reason to log. */
+ private int downloadStatusToFailureReason(int downloadStatusReason) {
+ switch (downloadStatusReason) {
+ case DownloadManager.PAUSED_WAITING_TO_RETRY:
+ case DownloadManager.PAUSED_WAITING_FOR_NETWORK:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_DEVICE_OFFLINE;
+ case DownloadManager.ERROR_UNHANDLED_HTTP_CODE:
+ case DownloadManager.ERROR_HTTP_DATA_ERROR:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_HTTP_ERROR;
+ case DownloadManager.ERROR_TOO_MANY_REDIRECTS:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_TOO_MANY_REDIRECTS;
+ case DownloadManager.ERROR_CANNOT_RESUME:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_DOWNLOAD_CANNOT_RESUME;
+ case DownloadManager.ERROR_INSUFFICIENT_SPACE:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_NO_DISK_SPACE;
+ case DownloadManager.PAUSED_QUEUED_FOR_WIFI:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__PENDING_WAITING_FOR_WIFI;
+ default:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_UNKNOWN;
+ }
+ }
+
+ /** Converts the local enum to the corresponding auto-generated one used by CTStatsLog. */
+ private int localEnumToStatsLogEnum(CTLogListUpdateState updateState) {
+ switch (updateState) {
+ case HTTP_ERROR:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_HTTP_ERROR;
+ case PUBLIC_KEY_NOT_FOUND:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_PUBLIC_KEY_NOT_FOUND;
+ case SIGNATURE_INVALID:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_INVALID;
+ case SIGNATURE_NOT_FOUND:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_NOT_FOUND;
+ case SIGNATURE_VERIFICATION_FAILED:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_VERIFICATION;
+ case VERSION_ALREADY_EXISTS:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_VERSION_ALREADY_EXISTS;
+ case UNKNOWN_STATE:
+ default:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_UNKNOWN;
+ }
+ }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
index 2a27204..a71ff7c 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
@@ -18,7 +18,6 @@
import static android.security.Flags.certificateTransparencyConfiguration;
-import static com.android.net.ct.flags.Flags.certificateTransparencyJob;
import static com.android.net.ct.flags.Flags.certificateTransparencyService;
import android.annotation.RequiresApi;
@@ -26,45 +25,48 @@
import android.net.ct.ICertificateTransparencyManager;
import android.os.Build;
import android.provider.DeviceConfig;
+import android.provider.DeviceConfig.Properties;
+import android.util.Log;
import com.android.server.SystemService;
+import java.util.concurrent.Executors;
+
/** Implementation of the Certificate Transparency service. */
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
-public class CertificateTransparencyService extends ICertificateTransparencyManager.Stub {
+public class CertificateTransparencyService extends ICertificateTransparencyManager.Stub
+ implements DeviceConfig.OnPropertiesChangedListener {
- private final CertificateTransparencyFlagsListener mFlagsListener;
+ private static final String TAG = "CertificateTransparencyService";
+
private final CertificateTransparencyJob mCertificateTransparencyJob;
/**
* @return true if the CertificateTransparency service is enabled.
*/
public static boolean enabled(Context context) {
- return DeviceConfig.getBoolean(
- Config.NAMESPACE_NETWORK_SECURITY,
- Config.FLAG_SERVICE_ENABLED,
- /* defaultValue= */ true)
- && certificateTransparencyService()
- && certificateTransparencyConfiguration();
+ return certificateTransparencyService() && certificateTransparencyConfiguration();
}
/** Creates a new {@link CertificateTransparencyService} object. */
public CertificateTransparencyService(Context context) {
DataStore dataStore = new DataStore(Config.PREFERENCES_FILE);
- DownloadHelper downloadHelper = new DownloadHelper(context);
- SignatureVerifier signatureVerifier = new SignatureVerifier(context);
- CertificateTransparencyDownloader downloader =
- new CertificateTransparencyDownloader(
+
+ mCertificateTransparencyJob =
+ new CertificateTransparencyJob(
context,
dataStore,
- downloadHelper,
- signatureVerifier,
- new CertificateTransparencyInstaller(),
- new CertificateTransparencyLogger());
- mFlagsListener =
- new CertificateTransparencyFlagsListener(dataStore, signatureVerifier, downloader);
- mCertificateTransparencyJob =
- new CertificateTransparencyJob(context, dataStore, downloader);
+ new CertificateTransparencyDownloader(
+ context,
+ dataStore,
+ new DownloadHelper(context),
+ new SignatureVerifier(context),
+ new CertificateTransparencyLoggerImpl(dataStore)),
+ new CompatibilityVersion(
+ Config.COMPATIBILITY_VERSION,
+ Config.URL_SIGNATURE,
+ Config.URL_LOG_LIST,
+ Config.CT_ROOT_DIRECTORY_PATH));
}
/**
@@ -75,13 +77,44 @@
public void onBootPhase(int phase) {
switch (phase) {
case SystemService.PHASE_BOOT_COMPLETED:
- if (certificateTransparencyJob()) {
- mCertificateTransparencyJob.initialize();
- } else {
- mFlagsListener.initialize();
- }
+ DeviceConfig.addOnPropertiesChangedListener(
+ Config.NAMESPACE_NETWORK_SECURITY,
+ Executors.newSingleThreadExecutor(),
+ this);
+ onPropertiesChanged(
+ new Properties.Builder(Config.NAMESPACE_NETWORK_SECURITY).build());
break;
default:
}
}
+
+ @Override
+ public void onPropertiesChanged(Properties properties) {
+ if (!Config.NAMESPACE_NETWORK_SECURITY.equals(properties.getNamespace())) {
+ return;
+ }
+
+ if (DeviceConfig.getBoolean(
+ Config.NAMESPACE_NETWORK_SECURITY,
+ Config.FLAG_SERVICE_ENABLED,
+ /* defaultValue= */ true)) {
+ startService();
+ } else {
+ stopService();
+ }
+ }
+
+ private void startService() {
+ if (Config.DEBUG) {
+ Log.d(TAG, "CertificateTransparencyService start");
+ }
+ mCertificateTransparencyJob.schedule();
+ }
+
+ private void stopService() {
+ if (Config.DEBUG) {
+ Log.d(TAG, "CertificateTransparencyService stop");
+ }
+ mCertificateTransparencyJob.cancel();
+ }
}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java b/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
index 27488b5..9d60163 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
@@ -15,58 +15,95 @@
*/
package com.android.server.net.ct;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.annotation.RequiresApi;
+import android.os.Build;
import android.system.ErrnoException;
import android.system.Os;
+import android.util.Log;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
/** Represents a compatibility version directory. */
+@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
class CompatibilityVersion {
+ private static final String TAG = "CompatibilityVersion";
+
static final String LOGS_DIR_PREFIX = "logs-";
static final String LOGS_LIST_FILE_NAME = "log_list.json";
+ static final String CURRENT_LOGS_DIR_SYMLINK_NAME = "current";
- private static final String CURRENT_LOGS_DIR_SYMLINK_NAME = "current";
+ private final String mCompatVersion;
+ private final String mMetadataUrl;
+ private final String mContentUrl;
private final File mRootDirectory;
+ private final File mVersionDirectory;
private final File mCurrentLogsDirSymlink;
- private File mCurrentLogsDir = null;
-
- CompatibilityVersion(File rootDirectory) {
+ CompatibilityVersion(
+ String compatVersion, String metadataUrl, String contentUrl, File rootDirectory) {
+ mCompatVersion = compatVersion;
+ mMetadataUrl = metadataUrl;
+ mContentUrl = contentUrl;
mRootDirectory = rootDirectory;
- mCurrentLogsDirSymlink = new File(mRootDirectory, CURRENT_LOGS_DIR_SYMLINK_NAME);
+ mVersionDirectory = new File(rootDirectory, compatVersion);
+ mCurrentLogsDirSymlink = new File(mVersionDirectory, CURRENT_LOGS_DIR_SYMLINK_NAME);
+ }
+
+ CompatibilityVersion(
+ String compatVersion, String metadataUrl, String contentUrl, String rootDirectoryPath) {
+ this(compatVersion, metadataUrl, contentUrl, new File(rootDirectoryPath));
}
/**
* Installs a log list within this compatibility version directory.
*
* @param newContent an input stream providing the log list
- * @param version the version number of the log list
* @return true if the log list was installed successfully, false otherwise.
* @throws IOException if the list cannot be saved in the CT directory.
*/
- boolean install(InputStream newContent, String version) throws IOException {
- // To support atomically replacing the old configuration directory with the new there's a
- // bunch of steps. We create a new directory with the logs and then do an atomic update of
- // the current symlink to point to the new directory.
- // 1. Ensure that the root directory exists and is readable.
- DirectoryUtils.makeDir(mRootDirectory);
+ boolean install(InputStream newContent) throws IOException {
+ String content = new String(newContent.readAllBytes(), UTF_8);
+ try {
+ return install(
+ new ByteArrayInputStream(content.getBytes()),
+ new JSONObject(content).getString("version"));
+ } catch (JSONException e) {
+ Log.e(TAG, "invalid log list format", e);
+ return false;
+ }
+ }
- File newLogsDir = new File(mRootDirectory, LOGS_DIR_PREFIX + version);
+ private boolean install(InputStream newContent, String version) throws IOException {
+ // To support atomically replacing the old configuration directory with the new
+ // there's a bunch of steps. We create a new directory with the logs and then do
+ // an atomic update of the current symlink to point to the new directory.
+ // 1. Ensure the path to the root and version directories exist and are readable.
+ DirectoryUtils.makeDir(mRootDirectory);
+ DirectoryUtils.makeDir(mVersionDirectory);
+
+ File newLogsDir = new File(mVersionDirectory, LOGS_DIR_PREFIX + version);
// 2. Handle the corner case where the new directory already exists.
if (newLogsDir.exists()) {
- // If the symlink has already been updated then the update died between steps 6 and 7
- // and so we cannot delete the directory since it is in use.
+ // If the symlink has already been updated then the update died between steps 6
+ // and 7 and so we cannot delete the directory since it is in use.
if (newLogsDir.getCanonicalPath().equals(mCurrentLogsDirSymlink.getCanonicalPath())) {
+ Log.i(TAG, newLogsDir + " already exists, skipping install.");
deleteOldLogDirectories();
return false;
}
- // If the symlink has not been updated then the previous installation failed and this is
- // a re-attempt. Clean-up leftover files and try again.
+ // If the symlink has not been updated then the previous installation failed and
+ // this is a re-attempt. Clean-up leftover files and try again.
DirectoryUtils.removeDir(newLogsDir);
}
try {
@@ -80,8 +117,8 @@
}
DirectoryUtils.setWorldReadable(logListFile);
- // 5. Create temp symlink. We rename this to the target symlink to get an atomic update.
- File tempSymlink = new File(mRootDirectory, "new_symlink");
+ // 5. Create temp symlink. We rename to the target symlink for an atomic update.
+ File tempSymlink = new File(mVersionDirectory, "new_symlink");
try {
Os.symlink(newLogsDir.getCanonicalPath(), tempSymlink.getCanonicalPath());
} catch (ErrnoException e) {
@@ -95,17 +132,33 @@
throw e;
}
// 7. Cleanup
- mCurrentLogsDir = newLogsDir;
+ Log.i(TAG, "New logs installed at " + newLogsDir);
deleteOldLogDirectories();
return true;
}
- File getRootDir() {
- return mRootDirectory;
+ String getCompatVersion() {
+ return mCompatVersion;
}
- File getLogsDir() {
- return mCurrentLogsDir;
+ String getMetadataUrl() {
+ return mMetadataUrl;
+ }
+
+ String getMetadataPropertyName() {
+ return mCompatVersion + "_" + Config.METADATA_DOWNLOAD_ID;
+ }
+
+ String getContentUrl() {
+ return mContentUrl;
+ }
+
+ String getContentPropertyName() {
+ return mCompatVersion + "_" + Config.CONTENT_DOWNLOAD_ID;
+ }
+
+ File getVersionDir() {
+ return mVersionDirectory;
}
File getLogsDirSymlink() {
@@ -113,19 +166,21 @@
}
File getLogsFile() {
- return new File(mCurrentLogsDir, LOGS_LIST_FILE_NAME);
+ return new File(mCurrentLogsDirSymlink, LOGS_LIST_FILE_NAME);
}
- boolean delete() {
- return DirectoryUtils.removeDir(mRootDirectory);
+ void delete() {
+ if (!DirectoryUtils.removeDir(mVersionDirectory)) {
+ Log.w(TAG, "Could not delete compatibility version directory " + mVersionDirectory);
+ }
}
private void deleteOldLogDirectories() throws IOException {
- if (!mRootDirectory.exists()) {
+ if (!mVersionDirectory.exists()) {
return;
}
File currentTarget = mCurrentLogsDirSymlink.getCanonicalFile();
- for (File file : mRootDirectory.listFiles()) {
+ for (File file : mVersionDirectory.listFiles()) {
if (!currentTarget.equals(file.getCanonicalFile())
&& file.getName().startsWith(LOGS_DIR_PREFIX)) {
DirectoryUtils.removeDir(file);
diff --git a/networksecurity/service/src/com/android/server/net/ct/Config.java b/networksecurity/service/src/com/android/server/net/ct/Config.java
index aafee60..5fdba09 100644
--- a/networksecurity/service/src/com/android/server/net/ct/Config.java
+++ b/networksecurity/service/src/com/android/server/net/ct/Config.java
@@ -48,11 +48,8 @@
// properties
static final String VERSION = "version";
- static final String CONTENT_URL = "content_url";
static final String CONTENT_DOWNLOAD_ID = "content_download_id";
- static final String METADATA_URL = "metadata_url";
static final String METADATA_DOWNLOAD_ID = "metadata_download_id";
- static final String PUBLIC_KEY_URL = "public_key_url";
static final String PUBLIC_KEY_DOWNLOAD_ID = "public_key_download_id";
static final String LOG_LIST_UPDATE_FAILURE_COUNT = "log_list_update_failure_count";
@@ -61,7 +58,4 @@
static final String URL_LOG_LIST = URL_PREFIX + "log_list.json";
static final String URL_SIGNATURE = URL_PREFIX + "log_list.sig";
static final String URL_PUBLIC_KEY = URL_PREFIX + "log_list.pub";
-
- // Threshold amounts
- static final int LOG_LIST_UPDATE_FAILURE_THRESHOLD = 10;
}
diff --git a/networksecurity/service/src/com/android/server/net/ct/DataStore.java b/networksecurity/service/src/com/android/server/net/ct/DataStore.java
index 3779269..8180316 100644
--- a/networksecurity/service/src/com/android/server/net/ct/DataStore.java
+++ b/networksecurity/service/src/com/android/server/net/ct/DataStore.java
@@ -57,6 +57,11 @@
}
}
+ boolean delete() {
+ clear();
+ return mPropertyFile.delete();
+ }
+
long getPropertyLong(String key, long defaultValue) {
return Optional.ofNullable(getProperty(key)).map(Long::parseLong).orElse(defaultValue);
}
diff --git a/networksecurity/service/src/com/android/server/net/ct/LogListUpdateStatus.java b/networksecurity/service/src/com/android/server/net/ct/LogListUpdateStatus.java
new file mode 100644
index 0000000..3d05857
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/LogListUpdateStatus.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.net.ct;
+
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.PUBLIC_KEY_NOT_FOUND;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_INVALID;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_NOT_FOUND;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_VERIFICATION_FAILED;
+
+import com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.Optional;
+
+/** Class to represent the signature verification status for Certificate Transparency. */
+@AutoValue
+public abstract class LogListUpdateStatus {
+
+ abstract CTLogListUpdateState state();
+
+ abstract String signature();
+
+ abstract long logListTimestamp();
+
+ abstract int httpErrorStatusCode();
+
+ abstract Optional<Integer> downloadStatus();
+
+ boolean isSignatureVerified() {
+ // Check that none of the signature verification failures have been set as the state
+ return state() != PUBLIC_KEY_NOT_FOUND
+ && state() != SIGNATURE_INVALID
+ && state() != SIGNATURE_NOT_FOUND
+ && state() != SIGNATURE_VERIFICATION_FAILED;
+ }
+
+ boolean hasSignature() {
+ return signature() != null && signature().length() > 0;
+ }
+
+ @AutoValue.Builder
+ abstract static class Builder {
+ abstract Builder setState(CTLogListUpdateState updateState);
+
+ abstract Builder setSignature(String signature);
+
+ abstract Builder setLogListTimestamp(long timestamp);
+
+ abstract Builder setHttpErrorStatusCode(int httpStatusCode);
+
+ abstract Builder setDownloadStatus(Optional<Integer> downloadStatus);
+
+ abstract LogListUpdateStatus build();
+ }
+
+ abstract LogListUpdateStatus.Builder toBuilder();
+
+ static Builder builder() {
+ return new AutoValue_LogListUpdateStatus.Builder()
+ .setState(CTLogListUpdateState.UNKNOWN_STATE)
+ .setSignature("")
+ .setLogListTimestamp(0L)
+ .setHttpErrorStatusCode(0)
+ .setDownloadStatus(Optional.empty());
+ }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java b/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
index 0b775ca..3ba56db 100644
--- a/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
+++ b/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
@@ -15,17 +15,24 @@
*/
package com.android.server.net.ct;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.PUBLIC_KEY_NOT_FOUND;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_INVALID;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_NOT_FOUND;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_VERIFICATION_FAILED;
+
import android.annotation.NonNull;
import android.annotation.RequiresApi;
import android.content.ContentResolver;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
+import android.util.Log;
import androidx.annotation.VisibleForTesting;
import java.io.IOException;
import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
@@ -40,6 +47,7 @@
public class SignatureVerifier {
private final Context mContext;
+ private static final String TAG = "SignatureVerifier";
@NonNull private Optional<PublicKey> mPublicKey = Optional.empty();
@@ -63,10 +71,15 @@
}
void setPublicKey(String publicKey) throws GeneralSecurityException {
+ byte[] decodedPublicKey = null;
+ try {
+ decodedPublicKey = Base64.getDecoder().decode(publicKey);
+ } catch (IllegalArgumentException e) {
+ throw new GeneralSecurityException("Invalid public key base64 encoding", e);
+ }
setPublicKey(
KeyFactory.getInstance("RSA")
- .generatePublic(
- new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey))));
+ .generatePublic(new X509EncodedKeySpec(decodedPublicKey)));
}
@VisibleForTesting
@@ -74,18 +87,54 @@
mPublicKey = Optional.of(publicKey);
}
- boolean verify(Uri file, Uri signature) throws GeneralSecurityException, IOException {
+ LogListUpdateStatus verify(Uri file, Uri signature) {
+ LogListUpdateStatus.Builder statusBuilder = LogListUpdateStatus.builder();
+
if (!mPublicKey.isPresent()) {
- throw new InvalidKeyException("Missing public key for signature verification");
+ statusBuilder.setState(PUBLIC_KEY_NOT_FOUND);
+ Log.e(TAG, "No public key found for log list verification");
+ return statusBuilder.build();
}
- Signature verifier = Signature.getInstance("SHA256withRSA");
- verifier.initVerify(mPublicKey.get());
+
ContentResolver contentResolver = mContext.getContentResolver();
try (InputStream fileStream = contentResolver.openInputStream(file);
InputStream signatureStream = contentResolver.openInputStream(signature)) {
+ Signature verifier = Signature.getInstance("SHA256withRSA");
+ verifier.initVerify(mPublicKey.get());
verifier.update(fileStream.readAllBytes());
- return verifier.verify(signatureStream.readAllBytes());
+
+ byte[] signatureBytes = signatureStream.readAllBytes();
+ try {
+ byte[] decodedSigBytes = Base64.getDecoder().decode(signatureBytes);
+ statusBuilder.setSignature(new String(decodedSigBytes, StandardCharsets.UTF_8));
+
+ 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.setSignature(new String(signatureBytes, StandardCharsets.UTF_8));
+ statusBuilder.setState(SIGNATURE_INVALID);
+ return statusBuilder.build();
+ }
+ } catch (InvalidKeyException e) {
+ Log.e(TAG, "Signature invalid for log list verification", e);
+ statusBuilder.setState(SIGNATURE_INVALID);
+ return statusBuilder.build();
+ } catch (IOException | GeneralSecurityException e) {
+ Log.e(TAG, "Could not verify new log list", e);
+ statusBuilder.setState(SIGNATURE_VERIFICATION_FAILED);
+ return statusBuilder.build();
}
+
+ // Double check if the signature is empty that we set the state correctly
+ if (!statusBuilder.build().hasSignature()) {
+ statusBuilder.setState(SIGNATURE_NOT_FOUND);
+ }
+
+ return statusBuilder.build();
}
}
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 3a359f4..08704d1 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
@@ -13,18 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package com.android.server.net.ct;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_NO_DISK_SPACE;
-import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_VERSION_ALREADY_EXISTS;
-
+import static com.google.common.io.Files.toByteArray;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -42,6 +37,8 @@
import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState;
+
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.After;
@@ -49,6 +46,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -58,6 +56,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
@@ -65,21 +64,23 @@
import java.security.PublicKey;
import java.security.Signature;
import java.util.Base64;
+import java.util.Optional;
/** Tests for the {@link CertificateTransparencyDownloader}. */
@RunWith(JUnit4.class)
public class CertificateTransparencyDownloaderTest {
@Mock private DownloadManager mDownloadManager;
- @Mock private CertificateTransparencyInstaller mCertificateTransparencyInstaller;
@Mock private CertificateTransparencyLogger mLogger;
+ private ArgumentCaptor<LogListUpdateStatus> mUpdateStatusCaptor =
+ ArgumentCaptor.forClass(LogListUpdateStatus.class);
private PrivateKey mPrivateKey;
private PublicKey mPublicKey;
private Context mContext;
- private File mTempFile;
private DataStore mDataStore;
private SignatureVerifier mSignatureVerifier;
+ private CompatibilityVersion mCompatVersion;
private CertificateTransparencyDownloader mCertificateTransparencyDownloader;
private long mNextDownloadId = 666;
@@ -93,8 +94,7 @@
mPublicKey = keyPair.getPublic();
mContext = InstrumentationRegistry.getInstrumentation().getContext();
- mTempFile = File.createTempFile("datastore-test", ".properties");
- mDataStore = new DataStore(mTempFile);
+ mDataStore = new DataStore(File.createTempFile("datastore-test", ".properties"));
mSignatureVerifier = new SignatureVerifier(mContext);
mCertificateTransparencyDownloader =
new CertificateTransparencyDownloader(
@@ -102,56 +102,64 @@
mDataStore,
new DownloadHelper(mDownloadManager),
mSignatureVerifier,
- mCertificateTransparencyInstaller,
mLogger);
+ mCompatVersion =
+ new CompatibilityVersion(
+ /* compatVersion= */ "v666",
+ Config.URL_SIGNATURE,
+ Config.URL_LOG_LIST,
+ mContext.getFilesDir());
- prepareDataStore();
prepareDownloadManager();
+ mDataStore.load();
+ mCertificateTransparencyDownloader.addCompatibilityVersion(mCompatVersion);
}
@After
public void tearDown() {
- mTempFile.delete();
mSignatureVerifier.resetPublicKey();
+ mCompatVersion.delete();
+ mDataStore.delete();
}
@Test
public void testDownloader_startPublicKeyDownload() {
assertThat(mCertificateTransparencyDownloader.hasPublicKeyDownloadId()).isFalse();
+
long downloadId = mCertificateTransparencyDownloader.startPublicKeyDownload();
assertThat(mCertificateTransparencyDownloader.hasPublicKeyDownloadId()).isTrue();
- assertThat(mCertificateTransparencyDownloader.isPublicKeyDownloadId(downloadId)).isTrue();
+ assertThat(mCertificateTransparencyDownloader.getPublicKeyDownloadId())
+ .isEqualTo(downloadId);
}
@Test
public void testDownloader_startMetadataDownload() {
assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
- long downloadId = mCertificateTransparencyDownloader.startMetadataDownload();
+
+ mCertificateTransparencyDownloader.startMetadataDownload();
assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isTrue();
- assertThat(mCertificateTransparencyDownloader.isMetadataDownloadId(downloadId)).isTrue();
}
@Test
public void testDownloader_startContentDownload() {
assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isFalse();
- long downloadId = mCertificateTransparencyDownloader.startContentDownload();
+
+ mCertificateTransparencyDownloader.startContentDownload(mCompatVersion);
assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isTrue();
- assertThat(mCertificateTransparencyDownloader.isContentDownloadId(downloadId)).isTrue();
}
@Test
public void testDownloader_publicKeyDownloadSuccess_updatePublicKey_startMetadataDownload()
throws Exception {
- long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
- setSuccessfulDownload(publicKeyId, writePublicKeyToFile(mPublicKey));
+ mCertificateTransparencyDownloader.startPublicKeyDownload();
assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(publicKeyId));
+ mContext, makePublicKeyDownloadCompleteIntent(writePublicKeyToFile(mPublicKey)));
assertThat(mSignatureVerifier.getPublicKey()).hasValue(mPublicKey);
assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isTrue();
@@ -161,14 +169,14 @@
public void
testDownloader_publicKeyDownloadSuccess_updatePublicKeyFail_doNotStartMetadataDownload()
throws Exception {
- long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
- setSuccessfulDownload(
- publicKeyId, writeToFile("i_am_not_a_base64_encoded_public_key".getBytes()));
+ mCertificateTransparencyDownloader.startPublicKeyDownload();
assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(publicKeyId));
+ mContext,
+ makePublicKeyDownloadCompleteIntent(
+ writeToFile("i_am_not_a_base64_encoded_public_key".getBytes())));
assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
@@ -176,301 +184,251 @@
@Test
public void testDownloader_publicKeyDownloadFail_doNotUpdatePublicKey() throws Exception {
- long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
- setFailedDownload(
- publicKeyId, // Failure cases where we give up on the download.
- DownloadManager.ERROR_INSUFFICIENT_SPACE,
- DownloadManager.ERROR_HTTP_DATA_ERROR);
- Intent downloadCompleteIntent = makeDownloadCompleteIntent(publicKeyId);
+ mCertificateTransparencyDownloader.startPublicKeyDownload();
assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
- mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
- mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makePublicKeyDownloadFailedIntent(DownloadManager.ERROR_INSUFFICIENT_SPACE));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makePublicKeyDownloadFailedIntent(DownloadManager.ERROR_HTTP_DATA_ERROR));
assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
}
@Test
- public void testDownloader_publicKeyDownloadFail_failureThresholdExceeded_logsFailure()
- throws Exception {
- long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
- // Set the failure count to just below the threshold
- mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT,
- Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
- setFailedDownload(
- publicKeyId, // Failure cases where we give up on the download.
- DownloadManager.ERROR_INSUFFICIENT_SPACE,
- DownloadManager.ERROR_HTTP_DATA_ERROR);
- Intent downloadCompleteIntent = makeDownloadCompleteIntent(publicKeyId);
+ public void testDownloader_publicKeyDownloadFail_logsFailure()
+ throws Exception {
+ mCertificateTransparencyDownloader.startPublicKeyDownload();
- mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makePublicKeyDownloadFailedIntent(DownloadManager.ERROR_INSUFFICIENT_SPACE));
- assertThat(mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
- .isEqualTo(Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD);
- verify(mLogger, times(1)).logCTLogListUpdateFailedEvent(
- CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_NO_DISK_SPACE,
- Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD
- );
- }
-
- @Test
- public void testDownloader_publicKeyDownloadFail_failureThresholdNotMet_doesNotLog()
- throws Exception {
- long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
- // Set the failure count to just below the threshold
- mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, 0);
- setFailedDownload(
- publicKeyId, // Failure cases where we give up on the download.
- DownloadManager.ERROR_INSUFFICIENT_SPACE,
- DownloadManager.ERROR_HTTP_DATA_ERROR);
- Intent downloadCompleteIntent = makeDownloadCompleteIntent(publicKeyId);
-
- mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
-
- assertThat(mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
- .isEqualTo(1);
- verify(mLogger, never()).logCTLogListUpdateFailedEvent(anyInt(), anyInt());
+ verify(mLogger, times(1))
+ .logCTLogListUpdateStateChangedEvent(
+ LogListUpdateStatus.builder()
+ .setDownloadStatus(
+ Optional.of(DownloadManager.ERROR_INSUFFICIENT_SPACE))
+ .build());
}
@Test
public void testDownloader_metadataDownloadSuccess_startContentDownload() {
- long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
- setSuccessfulDownload(metadataId, new File("log_list.sig"));
+ mCertificateTransparencyDownloader.startMetadataDownload();
assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isFalse();
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(metadataId));
+ mContext,
+ makeMetadataDownloadCompleteIntent(mCompatVersion, new File("log_list.sig")));
assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isTrue();
}
@Test
public void testDownloader_metadataDownloadFail_doNotStartContentDownload() {
- long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
- setFailedDownload(
- metadataId,
- // Failure cases where we give up on the download.
- DownloadManager.ERROR_INSUFFICIENT_SPACE,
- DownloadManager.ERROR_HTTP_DATA_ERROR);
- Intent downloadCompleteIntent = makeDownloadCompleteIntent(metadataId);
+ mCertificateTransparencyDownloader.startMetadataDownload();
assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isFalse();
- mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
- mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makeMetadataDownloadFailedIntent(
+ mCompatVersion, DownloadManager.ERROR_INSUFFICIENT_SPACE));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makeMetadataDownloadFailedIntent(
+ mCompatVersion, DownloadManager.ERROR_HTTP_DATA_ERROR));
assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isFalse();
}
@Test
- public void testDownloader_metadataDownloadFail_failureThresholdExceeded_logsFailure()
- throws Exception {
- long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
- // Set the failure count to just below the threshold
- mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT,
- Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
- setFailedDownload(
- metadataId,
- // Failure cases where we give up on the download.
- DownloadManager.ERROR_INSUFFICIENT_SPACE,
- DownloadManager.ERROR_HTTP_DATA_ERROR);
- Intent downloadCompleteIntent = makeDownloadCompleteIntent(metadataId);
-
- mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
-
- assertThat(mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
- .isEqualTo(Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD);
- verify(mLogger, times(1)).logCTLogListUpdateFailedEvent(
- CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_NO_DISK_SPACE,
- Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD
- );
- }
-
- @Test
- public void testDownloader_metadataDownloadFail_failureThresholdNotMet_doesNotLog()
- throws Exception {
- long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
- // Set the failure count to just below the threshold
- mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, 0);
- setFailedDownload(
- metadataId,
- // Failure cases where we give up on the download.
- DownloadManager.ERROR_INSUFFICIENT_SPACE,
- DownloadManager.ERROR_HTTP_DATA_ERROR);
- Intent downloadCompleteIntent = makeDownloadCompleteIntent(metadataId);
-
- mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
-
- assertThat(mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
- .isEqualTo(1);
- verify(mLogger, never()).logCTLogListUpdateFailedEvent(anyInt(), anyInt());
- }
-
- @Test
- public void testDownloader_contentDownloadSuccess_installSuccess_updateDataStore()
+ public void testDownloader_metadataDownloadFail_logsFailure()
throws Exception {
+ mCertificateTransparencyDownloader.startMetadataDownload();
+
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makeMetadataDownloadFailedIntent(
+ mCompatVersion, DownloadManager.ERROR_INSUFFICIENT_SPACE));
+
+ verify(mLogger, times(1))
+ .logCTLogListUpdateStateChangedEvent(
+ LogListUpdateStatus.builder()
+ .setDownloadStatus(
+ Optional.of(DownloadManager.ERROR_INSUFFICIENT_SPACE))
+ .build());
+ }
+
+ @Test
+ public void testDownloader_contentDownloadSuccess_installSuccess() throws Exception {
String newVersion = "456";
File logListFile = makeLogListFile(newVersion);
File metadataFile = sign(logListFile);
mSignatureVerifier.setPublicKey(mPublicKey);
- long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
- setSuccessfulDownload(metadataId, metadataFile);
- long contentId = mCertificateTransparencyDownloader.startContentDownload();
- setSuccessfulDownload(contentId, logListFile);
- when(mCertificateTransparencyInstaller.install(
- eq(Config.COMPATIBILITY_VERSION), any(), anyString()))
- .thenReturn(true);
+ mCertificateTransparencyDownloader.startMetadataDownload();
assertNoVersionIsInstalled();
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(contentId));
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
assertInstallSuccessful(newVersion);
}
@Test
public void testDownloader_contentDownloadFail_doNotInstall() throws Exception {
- long contentId = mCertificateTransparencyDownloader.startContentDownload();
- setFailedDownload(
- contentId,
- // Failure cases where we give up on the download.
- DownloadManager.ERROR_INSUFFICIENT_SPACE,
- DownloadManager.ERROR_HTTP_DATA_ERROR);
- Intent downloadCompleteIntent = makeDownloadCompleteIntent(contentId);
+ mCertificateTransparencyDownloader.startContentDownload(mCompatVersion);
- mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
- mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makeContentDownloadFailedIntent(
+ mCompatVersion, DownloadManager.ERROR_INSUFFICIENT_SPACE));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makeContentDownloadFailedIntent(
+ mCompatVersion, DownloadManager.ERROR_HTTP_DATA_ERROR));
- verify(mCertificateTransparencyInstaller, never()).install(any(), any(), any());
assertNoVersionIsInstalled();
}
@Test
- public void testDownloader_contentDownloadFail_failureThresholdExceeded_logsFailure()
- throws Exception {
- long contentId = mCertificateTransparencyDownloader.startContentDownload();
- // Set the failure count to just below the threshold
- mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT,
- Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
- setFailedDownload(
- contentId,
- // Failure cases where we give up on the download.
- DownloadManager.ERROR_INSUFFICIENT_SPACE,
- DownloadManager.ERROR_HTTP_DATA_ERROR);
- Intent downloadCompleteIntent = makeDownloadCompleteIntent(contentId);
-
- mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
-
- assertThat(mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
- .isEqualTo(Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD);
- verify(mLogger, times(1)).logCTLogListUpdateFailedEvent(
- CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_NO_DISK_SPACE,
- Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD
- );
- }
-
- @Test
- public void testDownloader_contentDownloadFail_failureThresholdNotMet_doesNotLog()
- throws Exception {
- long contentId = mCertificateTransparencyDownloader.startContentDownload();
- // Set the failure count to just below the threshold
- mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, 0);
- setFailedDownload(
- contentId,
- // Failure cases where we give up on the download.
- DownloadManager.ERROR_INSUFFICIENT_SPACE,
- DownloadManager.ERROR_HTTP_DATA_ERROR);
- Intent downloadCompleteIntent = makeDownloadCompleteIntent(contentId);
-
- mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
-
- assertThat(mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
- .isEqualTo(1);
- verify(mLogger, never()).logCTLogListUpdateFailedEvent(anyInt(), anyInt());
- }
-
- @Test
- public void testDownloader_contentDownloadSuccess_installFail_doNotUpdateDataStore()
+ public void testDownloader_contentDownloadFail_logsFailure()
throws Exception {
- File logListFile = makeLogListFile("456");
- File metadataFile = sign(logListFile);
+ mCertificateTransparencyDownloader.startContentDownload(mCompatVersion);
+
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makeContentDownloadFailedIntent(
+ mCompatVersion, DownloadManager.ERROR_INSUFFICIENT_SPACE));
+
+ verify(mLogger, times(1))
+ .logCTLogListUpdateStateChangedEvent(
+ LogListUpdateStatus.builder()
+ .setDownloadStatus(
+ Optional.of(DownloadManager.ERROR_INSUFFICIENT_SPACE))
+ .build());
+ }
+
+ @Test
+ public void testDownloader_contentDownloadSuccess_invalidLogList_installFails()
+ throws Exception {
+ File invalidLogListFile = writeToFile("not_a_json_log_list".getBytes());
+ File metadataFile = sign(invalidLogListFile);
mSignatureVerifier.setPublicKey(mPublicKey);
- long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
- setSuccessfulDownload(metadataId, metadataFile);
- long contentId = mCertificateTransparencyDownloader.startContentDownload();
- setSuccessfulDownload(contentId, logListFile);
- when(mCertificateTransparencyInstaller.install(
- eq(Config.COMPATIBILITY_VERSION), any(), anyString()))
- .thenReturn(false);
+ mCertificateTransparencyDownloader.startMetadataDownload();
assertNoVersionIsInstalled();
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(contentId));
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, invalidLogListFile));
assertNoVersionIsInstalled();
}
@Test
public void
- testDownloader_contentDownloadSuccess_installFail_failureThresholdExceeded_logsFailure()
+ testDownloader_contentDownloadSuccess_noPublicKeyFound_logsSingleFailure()
throws Exception {
File logListFile = makeLogListFile("456");
File metadataFile = sign(logListFile);
mSignatureVerifier.setPublicKey(mPublicKey);
- long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
- setSuccessfulDownload(metadataId, metadataFile);
- long contentId = mCertificateTransparencyDownloader.startContentDownload();
- setSuccessfulDownload(contentId, logListFile);
- // Set the failure count to just below the threshold
- mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT,
- Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
- when(mCertificateTransparencyInstaller.install(
- eq(Config.COMPATIBILITY_VERSION), any(), anyString()))
- .thenReturn(false);
+ mCertificateTransparencyDownloader.startMetadataDownload();
+ // Set the public key to be missing
+ mSignatureVerifier.resetPublicKey();
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(contentId));
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
- assertThat(mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
- .isEqualTo(Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD);
- verify(mLogger, times(1)).logCTLogListUpdateFailedEvent(
- CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_VERSION_ALREADY_EXISTS,
- Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD
- );
+ verify(mLogger, times(1))
+ .logCTLogListUpdateStateChangedEvent(mUpdateStatusCaptor.capture());
+ assertThat(mUpdateStatusCaptor.getValue().state())
+ .isEqualTo(CTLogListUpdateState.PUBLIC_KEY_NOT_FOUND);
}
@Test
public void
- testDownloader_contentDownloadSuccess_installFail_failureThresholdNotMet_doesNotLog()
+ testDownloader_contentDownloadSuccess_wrongSignatureAlgo_logsSingleFailure()
throws Exception {
+ // Arrange
File logListFile = makeLogListFile("456");
File metadataFile = sign(logListFile);
- mSignatureVerifier.setPublicKey(mPublicKey);
- long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
- setSuccessfulDownload(metadataId, metadataFile);
- long contentId = mCertificateTransparencyDownloader.startContentDownload();
- setSuccessfulDownload(contentId, logListFile);
- // Set the failure count to just below the threshold
- mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, 0);
- when(mCertificateTransparencyInstaller.install(
- eq(Config.COMPATIBILITY_VERSION), any(), anyString()))
- .thenReturn(false);
+ // Set the key to be deliberately wrong by using diff algorithm
+ KeyPairGenerator instance = KeyPairGenerator.getInstance("EC");
+ mSignatureVerifier.setPublicKey(instance.generateKeyPair().getPublic());
+
+ // Act
+ mCertificateTransparencyDownloader.startMetadataDownload();
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(contentId));
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
- assertThat(mDataStore.getPropertyInt(
- Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
- .isEqualTo(1);
- verify(mLogger, never()).logCTLogListUpdateFailedEvent(anyInt(), anyInt());
+ // Assert
+ verify(mLogger, times(1))
+ .logCTLogListUpdateStateChangedEvent(mUpdateStatusCaptor.capture());
+ assertThat(mUpdateStatusCaptor.getValue().state())
+ .isEqualTo(CTLogListUpdateState.SIGNATURE_INVALID);
+ }
+
+ @Test
+ public void
+ testDownloader_contentDownloadSuccess_signatureNotVerified_logsSingleFailure()
+ throws Exception {
+ // Arrange
+ File logListFile = makeLogListFile("456");
+ File metadataFile = sign(logListFile);
+
+ // Set the key to be deliberately wrong by using diff key pair
+ KeyPairGenerator instance = KeyPairGenerator.getInstance("RSA");
+ mSignatureVerifier.setPublicKey(instance.generateKeyPair().getPublic());
+
+ // Act
+ mCertificateTransparencyDownloader.startMetadataDownload();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
+
+ // Assert
+ byte[] signatureBytes = Base64.getDecoder().decode(toByteArray(metadataFile));
+ verify(mLogger, times(1))
+ .logCTLogListUpdateStateChangedEvent(mUpdateStatusCaptor.capture());
+ LogListUpdateStatus statusValue = mUpdateStatusCaptor.getValue();
+ assertThat(statusValue.state())
+ .isEqualTo(CTLogListUpdateState.SIGNATURE_VERIFICATION_FAILED);
+ assertThat(statusValue.signature())
+ .isEqualTo(new String(signatureBytes, StandardCharsets.UTF_8));
+ }
+
+ @Test
+ public void
+ testDownloader_contentDownloadSuccess_installFail_logsFailure()
+ throws Exception {
+ File invalidLogListFile = writeToFile("not_a_json_log_list".getBytes());
+ File metadataFile = sign(invalidLogListFile);
+ mSignatureVerifier.setPublicKey(mPublicKey);
+
+ mCertificateTransparencyDownloader.startMetadataDownload();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, invalidLogListFile));
+
+ byte[] signatureBytes = Base64.getDecoder().decode(toByteArray(metadataFile));
+ verify(mLogger, times(1))
+ .logCTLogListUpdateStateChangedEvent(mUpdateStatusCaptor.capture());
+ LogListUpdateStatus statusValue = mUpdateStatusCaptor.getValue();
+ assertThat(statusValue.state()).isEqualTo(CTLogListUpdateState.VERSION_ALREADY_EXISTS);
+ assertThat(statusValue.signature())
+ .isEqualTo(new String(signatureBytes, StandardCharsets.UTF_8));
}
@Test
@@ -479,17 +437,14 @@
File logListFile = makeLogListFile("456");
File metadataFile = File.createTempFile("log_list-wrong_metadata", "sig");
mSignatureVerifier.setPublicKey(mPublicKey);
- long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
- setSuccessfulDownload(metadataId, metadataFile);
- long contentId = mCertificateTransparencyDownloader.startContentDownload();
- setSuccessfulDownload(contentId, logListFile);
assertNoVersionIsInstalled();
+ mCertificateTransparencyDownloader.startMetadataDownload();
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(contentId));
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
- verify(mCertificateTransparencyInstaller, never())
- .install(eq(Config.COMPATIBILITY_VERSION), any(), anyString());
assertNoVersionIsInstalled();
}
@@ -499,17 +454,14 @@
File logListFile = makeLogListFile("456");
File metadataFile = sign(logListFile);
mSignatureVerifier.resetPublicKey();
- long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
- setSuccessfulDownload(metadataId, metadataFile);
- long contentId = mCertificateTransparencyDownloader.startContentDownload();
- setSuccessfulDownload(contentId, logListFile);
assertNoVersionIsInstalled();
+ mCertificateTransparencyDownloader.startMetadataDownload();
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(contentId));
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
- verify(mCertificateTransparencyInstaller, never())
- .install(eq(Config.COMPATIBILITY_VERSION), any(), anyString());
assertNoVersionIsInstalled();
}
@@ -523,52 +475,37 @@
assertNoVersionIsInstalled();
// 1. Start download of public key.
- long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
+ mCertificateTransparencyDownloader.startPublicKeyDownload();
- // 2. On successful public key download, set the key and start the metatadata download.
- setSuccessfulDownload(publicKeyId, publicKeyFile);
-
+ // 2. On successful public key download, set the key and start the metatadata
+ // download.
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(publicKeyId));
+ mContext, makePublicKeyDownloadCompleteIntent(publicKeyFile));
// 3. On successful metadata download, start the content download.
- long metadataId = mCertificateTransparencyDownloader.getMetadataDownloadId();
- setSuccessfulDownload(metadataId, metadataFile);
-
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(metadataId));
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
- // 4. On successful content download, verify the signature and install the new version.
- long contentId = mCertificateTransparencyDownloader.getContentDownloadId();
- setSuccessfulDownload(contentId, logListFile);
- when(mCertificateTransparencyInstaller.install(
- eq(Config.COMPATIBILITY_VERSION), any(), anyString()))
- .thenReturn(true);
-
+ // 4. On successful content download, verify the signature and install the new
+ // version.
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(contentId));
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
assertInstallSuccessful(newVersion);
}
private void assertNoVersionIsInstalled() {
- assertThat(mDataStore.getProperty(Config.VERSION)).isNull();
+ assertThat(mCompatVersion.getVersionDir().exists()).isFalse();
}
private void assertInstallSuccessful(String version) {
- assertThat(mDataStore.getProperty(Config.VERSION)).isEqualTo(version);
- }
-
- private Intent makeDownloadCompleteIntent(long downloadId) {
- return new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
- .putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId);
- }
-
- private void prepareDataStore() {
- mDataStore.load();
- mDataStore.setProperty(Config.CONTENT_URL, Config.URL_LOG_LIST);
- mDataStore.setProperty(Config.METADATA_URL, Config.URL_SIGNATURE);
- mDataStore.setProperty(Config.PUBLIC_KEY_URL, Config.URL_PUBLIC_KEY);
+ File logsDir =
+ new File(
+ mCompatVersion.getVersionDir(),
+ CompatibilityVersion.LOGS_DIR_PREFIX + version);
+ assertThat(logsDir.exists()).isTrue();
+ File logsFile = new File(logsDir, CompatibilityVersion.LOGS_LIST_FILE_NAME);
+ assertThat(logsFile.exists()).isTrue();
}
private void prepareDownloadManager() {
@@ -576,6 +513,32 @@
.thenAnswer(invocation -> mNextDownloadId++);
}
+ private Intent makePublicKeyDownloadCompleteIntent(File publicKeyfile) {
+ return makeDownloadCompleteIntent(
+ mCertificateTransparencyDownloader.getPublicKeyDownloadId(), publicKeyfile);
+ }
+
+ private Intent makeMetadataDownloadCompleteIntent(
+ CompatibilityVersion compatVersion, File signatureFile) {
+ return makeDownloadCompleteIntent(
+ mCertificateTransparencyDownloader.getMetadataDownloadId(compatVersion),
+ signatureFile);
+ }
+
+ private Intent makeContentDownloadCompleteIntent(
+ CompatibilityVersion compatVersion, File logListFile) {
+ return makeDownloadCompleteIntent(
+ mCertificateTransparencyDownloader.getContentDownloadId(compatVersion),
+ logListFile);
+ }
+
+ private Intent makeDownloadCompleteIntent(long downloadId, File file) {
+ when(mDownloadManager.query(any(Query.class))).thenReturn(makeSuccessfulDownloadCursor());
+ when(mDownloadManager.getUriForDownloadedFile(downloadId)).thenReturn(Uri.fromFile(file));
+ return new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
+ .putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId);
+ }
+
private Cursor makeSuccessfulDownloadCursor() {
MatrixCursor cursor =
new MatrixCursor(
@@ -586,9 +549,26 @@
return cursor;
}
- private void setSuccessfulDownload(long downloadId, File file) {
- when(mDownloadManager.query(any(Query.class))).thenReturn(makeSuccessfulDownloadCursor());
- when(mDownloadManager.getUriForDownloadedFile(downloadId)).thenReturn(Uri.fromFile(file));
+ private Intent makePublicKeyDownloadFailedIntent(int error) {
+ return makeDownloadFailedIntent(
+ mCertificateTransparencyDownloader.getPublicKeyDownloadId(), error);
+ }
+
+ private Intent makeMetadataDownloadFailedIntent(CompatibilityVersion compatVersion, int error) {
+ return makeDownloadFailedIntent(
+ mCertificateTransparencyDownloader.getMetadataDownloadId(compatVersion), error);
+ }
+
+ private Intent makeContentDownloadFailedIntent(CompatibilityVersion compatVersion, int error) {
+ return makeDownloadFailedIntent(
+ mCertificateTransparencyDownloader.getContentDownloadId(compatVersion), error);
+ }
+
+ private Intent makeDownloadFailedIntent(long downloadId, int error) {
+ when(mDownloadManager.query(any(Query.class))).thenReturn(makeFailedDownloadCursor(error));
+ when(mDownloadManager.getUriForDownloadedFile(downloadId)).thenReturn(null);
+ return new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
+ .putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId);
}
private Cursor makeFailedDownloadCursor(int error) {
@@ -601,16 +581,6 @@
return cursor;
}
- private void setFailedDownload(long downloadId, int... downloadManagerErrors) {
- Cursor first = makeFailedDownloadCursor(downloadManagerErrors[0]);
- Cursor[] others = new Cursor[downloadManagerErrors.length - 1];
- for (int i = 1; i < downloadManagerErrors.length; i++) {
- others[i - 1] = makeFailedDownloadCursor(downloadManagerErrors[i]);
- }
- when(mDownloadManager.query(any())).thenReturn(first, others);
- when(mDownloadManager.getUriForDownloadedFile(downloadId)).thenReturn(null);
- }
-
private File writePublicKeyToFile(PublicKey publicKey)
throws IOException, GeneralSecurityException {
return writeToFile(Base64.getEncoder().encode(publicKey.getEncoded()));
@@ -644,7 +614,7 @@
try (InputStream fileStream = new FileInputStream(file);
OutputStream outputStream = new FileOutputStream(signatureFile)) {
signer.update(fileStream.readAllBytes());
- outputStream.write(signer.sign());
+ outputStream.write(Base64.getEncoder().encode(signer.sign()));
}
return signatureFile;
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java
deleted file mode 100644
index 50d3f23..0000000
--- a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.server.net.ct;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-/** Tests for the {@link CertificateTransparencyInstaller}. */
-@RunWith(JUnit4.class)
-public class CertificateTransparencyInstallerTest {
-
- private static final String TEST_VERSION = "test-v1";
-
- private File mTestDir =
- new File(
- InstrumentationRegistry.getInstrumentation().getContext().getFilesDir(),
- "test-dir");
- private CertificateTransparencyInstaller mCertificateTransparencyInstaller =
- new CertificateTransparencyInstaller(mTestDir);
-
- @Before
- public void setUp() {
- mCertificateTransparencyInstaller.addCompatibilityVersion(TEST_VERSION);
- }
-
- @After
- public void tearDown() {
- mCertificateTransparencyInstaller.removeCompatibilityVersion(TEST_VERSION);
- DirectoryUtils.removeDir(mTestDir);
- }
-
- @Test
- public void testCompatibilityVersion_installSuccessful() throws IOException {
- assertThat(mTestDir.mkdir()).isTrue();
- String content = "i_am_compatible";
- String version = "i_am_version";
- CompatibilityVersion compatVersion =
- mCertificateTransparencyInstaller.getCompatibilityVersion(TEST_VERSION);
-
- try (InputStream inputStream = asStream(content)) {
- assertThat(compatVersion.install(inputStream, version)).isTrue();
- }
- File logsDir = compatVersion.getLogsDir();
- assertThat(logsDir.exists()).isTrue();
- assertThat(logsDir.isDirectory()).isTrue();
- assertThat(logsDir.getAbsolutePath())
- .startsWith(mTestDir.getAbsolutePath() + "/" + TEST_VERSION);
- File logsListFile = compatVersion.getLogsFile();
- assertThat(logsListFile.exists()).isTrue();
- assertThat(logsListFile.getAbsolutePath()).startsWith(logsDir.getAbsolutePath());
- assertThat(readAsString(logsListFile)).isEqualTo(content);
- File logsSymlink = compatVersion.getLogsDirSymlink();
- assertThat(logsSymlink.exists()).isTrue();
- assertThat(logsSymlink.isDirectory()).isTrue();
- assertThat(logsSymlink.getAbsolutePath())
- .startsWith(mTestDir.getAbsolutePath() + "/" + TEST_VERSION + "/current");
- assertThat(logsSymlink.getCanonicalPath()).isEqualTo(logsDir.getCanonicalPath());
-
- assertThat(compatVersion.delete()).isTrue();
- assertThat(logsDir.exists()).isFalse();
- assertThat(logsSymlink.exists()).isFalse();
- assertThat(logsListFile.exists()).isFalse();
- }
-
- @Test
- public void testCompatibilityVersion_versionInstalledFailed() throws IOException {
- assertThat(mTestDir.mkdir()).isTrue();
-
- CompatibilityVersion compatVersion =
- mCertificateTransparencyInstaller.getCompatibilityVersion(TEST_VERSION);
- File rootDir = compatVersion.getRootDir();
- assertThat(rootDir.mkdir()).isTrue();
-
- String existingVersion = "666";
- File existingLogDir =
- new File(rootDir, CompatibilityVersion.LOGS_DIR_PREFIX + existingVersion);
- assertThat(existingLogDir.mkdir()).isTrue();
-
- String existingContent = "somebody_tried_to_install_me_but_failed_halfway_through";
- File logsListFile = new File(existingLogDir, CompatibilityVersion.LOGS_LIST_FILE_NAME);
- assertThat(logsListFile.createNewFile()).isTrue();
- writeToFile(logsListFile, existingContent);
-
- String newContent = "i_am_the_real_content";
- try (InputStream inputStream = asStream(newContent)) {
- assertThat(compatVersion.install(inputStream, existingVersion)).isTrue();
- }
-
- assertThat(readAsString(logsListFile)).isEqualTo(newContent);
- }
-
- @Test
- public void testCertificateTransparencyInstaller_installSuccessfully() throws IOException {
- String content = "i_am_a_certificate_and_i_am_transparent";
- String version = "666";
-
- try (InputStream inputStream = asStream(content)) {
- assertThat(
- mCertificateTransparencyInstaller.install(
- TEST_VERSION, inputStream, version))
- .isTrue();
- }
-
- assertThat(mTestDir.exists()).isTrue();
- assertThat(mTestDir.isDirectory()).isTrue();
- CompatibilityVersion compatVersion =
- mCertificateTransparencyInstaller.getCompatibilityVersion(TEST_VERSION);
- File logsDir = compatVersion.getLogsDir();
- assertThat(logsDir.exists()).isTrue();
- assertThat(logsDir.isDirectory()).isTrue();
- assertThat(logsDir.getAbsolutePath())
- .startsWith(mTestDir.getAbsolutePath() + "/" + TEST_VERSION);
- File logsListFile = compatVersion.getLogsFile();
- assertThat(logsListFile.exists()).isTrue();
- assertThat(logsListFile.getAbsolutePath()).startsWith(logsDir.getAbsolutePath());
- assertThat(readAsString(logsListFile)).isEqualTo(content);
- }
-
- @Test
- public void testCertificateTransparencyInstaller_versionIsAlreadyInstalled()
- throws IOException {
- String existingVersion = "666";
- String existingContent = "i_was_already_installed_successfully";
- CompatibilityVersion compatVersion =
- mCertificateTransparencyInstaller.getCompatibilityVersion(TEST_VERSION);
-
- DirectoryUtils.makeDir(mTestDir);
- try (InputStream inputStream = asStream(existingContent)) {
- assertThat(compatVersion.install(inputStream, existingVersion)).isTrue();
- }
-
- try (InputStream inputStream = asStream("i_will_be_ignored")) {
- assertThat(
- mCertificateTransparencyInstaller.install(
- TEST_VERSION, inputStream, existingVersion))
- .isFalse();
- }
-
- assertThat(readAsString(compatVersion.getLogsFile())).isEqualTo(existingContent);
- }
-
- private static InputStream asStream(String string) throws IOException {
- return new ByteArrayInputStream(string.getBytes());
- }
-
- private static String readAsString(File file) throws IOException {
- return new String(new FileInputStream(file).readAllBytes());
- }
-
- private static void writeToFile(File file, String string) throws IOException {
- try (OutputStream out = new FileOutputStream(file)) {
- out.write(string.getBytes());
- }
- }
-}
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java
new file mode 100644
index 0000000..38fff48
--- /dev/null
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.net.ct;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/** Tests for the {@link CompatibilityVersion}. */
+@RunWith(JUnit4.class)
+public class CompatibilityVersionTest {
+
+ private static final String TEST_VERSION = "v123";
+
+ private final File mTestDir =
+ InstrumentationRegistry.getInstrumentation().getContext().getFilesDir();
+ private final CompatibilityVersion mCompatVersion =
+ new CompatibilityVersion(
+ TEST_VERSION, Config.URL_SIGNATURE, Config.URL_LOG_LIST, mTestDir);
+
+ @After
+ public void tearDown() {
+ mCompatVersion.delete();
+ }
+
+ @Test
+ public void testCompatibilityVersion_versionDirectory_setupSuccessful() {
+ File versionDir = mCompatVersion.getVersionDir();
+ assertThat(versionDir.exists()).isFalse();
+ assertThat(versionDir.getAbsolutePath()).startsWith(mTestDir.getAbsolutePath());
+ assertThat(versionDir.getAbsolutePath()).endsWith(TEST_VERSION);
+ }
+
+ @Test
+ public void testCompatibilityVersion_symlink_setupSuccessful() {
+ File dirSymlink = mCompatVersion.getLogsDirSymlink();
+ assertThat(dirSymlink.exists()).isFalse();
+ assertThat(dirSymlink.getAbsolutePath())
+ .startsWith(mCompatVersion.getVersionDir().getAbsolutePath());
+ }
+
+ @Test
+ public void testCompatibilityVersion_logsFile_setupSuccessful() {
+ File logsFile = mCompatVersion.getLogsFile();
+ assertThat(logsFile.exists()).isFalse();
+ assertThat(logsFile.getAbsolutePath())
+ .startsWith(mCompatVersion.getLogsDirSymlink().getAbsolutePath());
+ }
+
+ @Test
+ public void testCompatibilityVersion_installSuccessful() throws Exception {
+ String version = "i_am_version";
+ JSONObject logList = makeLogList(version, "i_am_content");
+
+ try (InputStream inputStream = asStream(logList)) {
+ assertThat(mCompatVersion.install(inputStream)).isTrue();
+ }
+
+ File logListFile = mCompatVersion.getLogsFile();
+ assertThat(logListFile.exists()).isTrue();
+ assertThat(logListFile.getCanonicalPath())
+ .isEqualTo(
+ // <path-to-test-files>/v123/logs-i_am_version/log_list.json
+ new File(
+ new File(
+ mCompatVersion.getVersionDir(),
+ CompatibilityVersion.LOGS_DIR_PREFIX + version),
+ CompatibilityVersion.LOGS_LIST_FILE_NAME)
+ .getCanonicalPath());
+ assertThat(logListFile.getAbsolutePath())
+ .isEqualTo(
+ // <path-to-test-files>/v123/current/log_list.json
+ new File(
+ new File(
+ mCompatVersion.getVersionDir(),
+ CompatibilityVersion.CURRENT_LOGS_DIR_SYMLINK_NAME),
+ CompatibilityVersion.LOGS_LIST_FILE_NAME)
+ .getAbsolutePath());
+ }
+
+ @Test
+ public void testCompatibilityVersion_deleteSuccessfully() throws Exception {
+ try (InputStream inputStream = asStream(makeLogList(/* version= */ "123"))) {
+ assertThat(mCompatVersion.install(inputStream)).isTrue();
+ }
+
+ mCompatVersion.delete();
+
+ assertThat(mCompatVersion.getLogsFile().exists()).isFalse();
+ }
+
+ @Test
+ public void testCompatibilityVersion_invalidLogList() throws Exception {
+ try (InputStream inputStream = new ByteArrayInputStream(("not_a_valid_list".getBytes()))) {
+ assertThat(mCompatVersion.install(inputStream)).isFalse();
+ }
+
+ assertThat(mCompatVersion.getLogsFile().exists()).isFalse();
+ }
+
+ @Test
+ public void testCompatibilityVersion_incompleteVersionExists_replacesOldVersion()
+ throws Exception {
+ String existingVersion = "666";
+ File existingLogDir =
+ new File(
+ mCompatVersion.getVersionDir(),
+ CompatibilityVersion.LOGS_DIR_PREFIX + existingVersion);
+ assertThat(existingLogDir.mkdirs()).isTrue();
+ File logsListFile = new File(existingLogDir, CompatibilityVersion.LOGS_LIST_FILE_NAME);
+ assertThat(logsListFile.createNewFile()).isTrue();
+
+ JSONObject newLogList = makeLogList(existingVersion, "i_am_the_real_content");
+ try (InputStream inputStream = asStream(newLogList)) {
+ assertThat(mCompatVersion.install(inputStream)).isTrue();
+ }
+
+ assertThat(readAsString(logsListFile)).isEqualTo(newLogList.toString());
+ }
+
+ @Test
+ public void testCompatibilityVersion_versionAlreadyExists_installFails() throws Exception {
+ String existingVersion = "666";
+ JSONObject existingLogList = makeLogList(existingVersion, "i_was_installed_successfully");
+ try (InputStream inputStream = asStream(existingLogList)) {
+ assertThat(mCompatVersion.install(inputStream)).isTrue();
+ }
+
+ try (InputStream inputStream = asStream(makeLogList(existingVersion, "i_am_ignored"))) {
+ assertThat(mCompatVersion.install(inputStream)).isFalse();
+ }
+
+ assertThat(readAsString(mCompatVersion.getLogsFile()))
+ .isEqualTo(existingLogList.toString());
+ }
+
+ private static InputStream asStream(JSONObject logList) throws IOException {
+ return new ByteArrayInputStream(logList.toString().getBytes());
+ }
+
+ private static JSONObject makeLogList(String version) throws JSONException {
+ return new JSONObject().put("version", version);
+ }
+
+ private static JSONObject makeLogList(String version, String content) throws JSONException {
+ return makeLogList(version).put("content", content);
+ }
+
+ private static String readAsString(File file) throws IOException {
+ try (InputStream in = new FileInputStream(file)) {
+ return new String(in.readAllBytes());
+ }
+ }
+}
diff --git a/remoteauth/OWNERS b/remoteauth/OWNERS
index 25a32b9..ee46c1c 100644
--- a/remoteauth/OWNERS
+++ b/remoteauth/OWNERS
@@ -2,7 +2,6 @@
# Bug template url: http://b/new?component=1145231&template=1715387
billyhuang@google.com
boetger@google.com
-casbor@google.com
derekjedral@google.com
dlm@google.com
igorzas@google.com
diff --git a/remoteauth/service/jni/Android.bp b/remoteauth/service/jni/Android.bp
index 57e3ec9..00cf2ba 100644
--- a/remoteauth/service/jni/Android.bp
+++ b/remoteauth/service/jni/Android.bp
@@ -25,7 +25,7 @@
],
prefer_rlib: true,
apex_available: [
- "com.android.remoteauth",
+ "com.android.tethering",
],
host_supported: true,
}
diff --git a/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs b/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
index 9add6df..1d43d38 100644
--- a/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
+++ b/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
@@ -140,6 +140,7 @@
}
impl Platform for JavaPlatform {
+ #[allow(clippy::unit_arg)]
fn send_request(
&mut self,
connection_id: i32,
diff --git a/remoteauth/service/jni/src/unique_jvm.rs b/remoteauth/service/jni/src/unique_jvm.rs
index 46cc361..ddbb16f 100644
--- a/remoteauth/service/jni/src/unique_jvm.rs
+++ b/remoteauth/service/jni/src/unique_jvm.rs
@@ -41,6 +41,7 @@
Ok(())
}
/// Gets a 'static reference to the unique JavaVM. Returns None if set_once() was never called.
+#[allow(static_mut_refs)]
pub(crate) fn get_static_ref() -> Option<&'static Arc<JavaVM>> {
// Safety: follows [this pattern](https://doc.rust-lang.org/std/sync/struct.Once.html).
// Modification to static mut is nested inside call_once.
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index 4a9410e..9b3c7ba 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -51,7 +51,6 @@
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.HandlerUtils;
import com.android.net.module.util.NetdUtils;
-import com.android.net.module.util.PermissionUtils;
import com.android.net.module.util.SharedLog;
import com.android.net.module.util.ip.NetlinkMonitor;
import com.android.net.module.util.netlink.NetlinkConstants;
@@ -398,6 +397,16 @@
return mFactory.hasInterface(iface);
}
+ private List<String> getAllInterfaces() {
+ final ArrayList<String> interfaces = new ArrayList<>(
+ List.of(mFactory.getAvailableInterfaces(/* includeRestricted */ true)));
+
+ if (mTetheringInterfaceMode == INTERFACE_MODE_SERVER && mTetheringInterface != null) {
+ interfaces.add(mTetheringInterface);
+ }
+ return interfaces;
+ }
+
String[] getClientModeInterfaces(boolean includeRestricted) {
return mFactory.getAvailableInterfaces(includeRestricted);
}
@@ -460,10 +469,16 @@
public void setIncludeTestInterfaces(boolean include) {
mHandler.post(() -> {
mIncludeTestInterfaces = include;
- if (!include) {
+ if (include) {
+ trackAvailableInterfaces();
+ } else {
removeTestData();
+ // remove all test interfaces
+ for (String iface : getAllInterfaces()) {
+ if (isValidEthernetInterface(iface)) continue;
+ stopTrackingInterface(iface);
+ }
}
- trackAvailableInterfaces();
});
}
@@ -624,7 +639,7 @@
nc = mNetworkCapabilities.get(hwAddress);
if (nc == null) {
final boolean isTestIface = iface.matches(TEST_IFACE_REGEXP);
- nc = createDefaultNetworkCapabilities(isTestIface);
+ nc = createDefaultNetworkCapabilities(isTestIface, /* overrideTransport */ null);
}
}
@@ -739,9 +754,13 @@
*/
private void parseEthernetConfig(String configString) {
final EthernetTrackerConfig config = createEthernetTrackerConfig(configString);
- NetworkCapabilities nc = createNetworkCapabilities(
- !TextUtils.isEmpty(config.mCapabilities) /* clear default capabilities */,
- config.mCapabilities, config.mTransport).build();
+ NetworkCapabilities nc;
+ if (TextUtils.isEmpty(config.mCapabilities)) {
+ boolean isTestIface = config.mIface.matches(TEST_IFACE_REGEXP);
+ nc = createDefaultNetworkCapabilities(isTestIface, config.mTransport);
+ } else {
+ nc = createNetworkCapabilities(config.mCapabilities, config.mTransport).build();
+ }
mNetworkCapabilities.put(config.mIface, nc);
if (null != config.mIpConfig) {
@@ -756,15 +775,16 @@
return new EthernetTrackerConfig(configString.split(";", /* limit of tokens */ 4));
}
- private static NetworkCapabilities createDefaultNetworkCapabilities(boolean isTestIface) {
- NetworkCapabilities.Builder builder = createNetworkCapabilities(
- false /* clear default capabilities */, null, null)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
+ private static NetworkCapabilities createDefaultNetworkCapabilities(
+ boolean isTestIface, @Nullable String overrideTransport) {
+ NetworkCapabilities.Builder builder =
+ createNetworkCapabilities(/* commaSeparatedCapabilities */ null, overrideTransport)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
if (isTestIface) {
builder.addTransportType(NetworkCapabilities.TRANSPORT_TEST);
@@ -778,7 +798,6 @@
/**
* Parses a static list of network capabilities
*
- * @param clearDefaultCapabilities Indicates whether or not to clear any default capabilities
* @param commaSeparatedCapabilities A comma separated string list of integer encoded
* NetworkCapability.NET_CAPABILITY_* values
* @param overrideTransport A string representing a single integer encoded override transport
@@ -788,12 +807,12 @@
*/
@VisibleForTesting
static NetworkCapabilities.Builder createNetworkCapabilities(
- boolean clearDefaultCapabilities, @Nullable String commaSeparatedCapabilities,
- @Nullable String overrideTransport) {
+ @Nullable String commaSeparatedCapabilities, @Nullable String overrideTransport) {
- final NetworkCapabilities.Builder builder = clearDefaultCapabilities
- ? NetworkCapabilities.Builder.withoutDefaultCapabilities()
- : new NetworkCapabilities.Builder();
+ final NetworkCapabilities.Builder builder =
+ TextUtils.isEmpty(commaSeparatedCapabilities)
+ ? new NetworkCapabilities.Builder()
+ : NetworkCapabilities.Builder.withoutDefaultCapabilities();
// Determine the transport type. If someone has tried to define an override transport then
// attempt to add it. Since we can only have one override, all errors with it will
@@ -949,17 +968,7 @@
if (mIsEthernetEnabled == enabled) return;
mIsEthernetEnabled = enabled;
-
- // Interface in server mode should also be included.
- ArrayList<String> interfaces =
- new ArrayList<>(
- List.of(mFactory.getAvailableInterfaces(/* includeRestricted */ true)));
-
- if (mTetheringInterfaceMode == INTERFACE_MODE_SERVER) {
- interfaces.add(mTetheringInterface);
- }
-
- for (String iface : interfaces) {
+ for (String iface : getAllInterfaces()) {
setInterfaceUpState(iface, enabled);
}
broadcastEthernetStateChange(mIsEthernetEnabled);
diff --git a/service/Android.bp b/service/Android.bp
index 2659ebf..8b469e4 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -113,7 +113,6 @@
":services.connectivity-netstats-jni-sources",
"jni/com_android_server_connectivity_ClatCoordinator.cpp",
"jni/com_android_server_ServiceManagerWrapper.cpp",
- "jni/com_android_server_TestNetworkService.cpp",
"jni/onload.cpp",
],
header_libs: [
@@ -125,7 +124,7 @@
"libmodules-utils-build",
"libnetjniutils",
"libnet_utils_device_common_bpfjni",
- "libnet_utils_device_common_timerfdjni",
+ "libserviceconnectivityjni",
"netd_aidl_interface-lateststable-ndk",
],
shared_libs: [
@@ -162,6 +161,7 @@
],
libs: [
"framework-annotations-lib",
+ "framework-bluetooth.stubs.module_lib",
"framework-configinfrastructure.stubs.module_lib",
"framework-connectivity-pre-jarjar",
// The framework-connectivity-t library is only available on T+ platforms
diff --git a/service/ServiceConnectivityResources/res/values/config_thread.xml b/service/ServiceConnectivityResources/res/values/config_thread.xml
index 4027038..d1d9e52 100644
--- a/service/ServiceConnectivityResources/res/values/config_thread.xml
+++ b/service/ServiceConnectivityResources/res/values/config_thread.xml
@@ -71,4 +71,18 @@
-->
<string-array name="config_thread_mdns_vendor_specific_txts">
</string-array>
+
+ <!-- Whether to enable / start SRP server only when border routing is ready. SRP server and
+ border routing are mandatory features required by a Thread Border Router, and it takes 10 to
+ 20 seconds to establish border routing. Starting SRP server earlier is useful for use cases
+ where the user needs to know what are the devices in the network before actually needs to reach
+ to the devices, or reaching to Thread end devices doesn't require border routing to work.
+ -->
+ <bool name="config_thread_srp_server_wait_for_border_routing_enabled">true</bool>
+
+ <!-- Whether this border router will automatically join the previous connected network after
+ device reboots. Setting this value to false can allow the user to control the lifecycle of
+ the Thread border router state on this device.
+ -->
+ <bool name="config_thread_border_router_auto_join_enabled">true</bool>
</resources>
diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml
index fbaae05..7ac86aa 100644
--- a/service/ServiceConnectivityResources/res/values/overlayable.xml
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -52,6 +52,8 @@
<item type="string" name="config_thread_vendor_oui" />
<item type="string" name="config_thread_model_name" />
<item type="array" name="config_thread_mdns_vendor_specific_txts" />
+ <item type="bool" name="config_thread_srp_server_wait_for_border_routing_enabled" />
+ <item type="bool" name="config_thread_border_router_auto_join_enabled" />
</policy>
</overlayable>
</resources>
diff --git a/service/jni/com_android_server_TestNetworkService.cpp b/service/jni/com_android_server_TestNetworkService.cpp
deleted file mode 100644
index 08d31a3..0000000
--- a/service/jni/com_android_server_TestNetworkService.cpp
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#define LOG_NDEBUG 0
-
-#define LOG_TAG "TestNetworkServiceJni"
-
-#include <arpa/inet.h>
-#include <errno.h>
-#include <fcntl.h>
-#include <linux/if.h>
-#include <linux/if_tun.h>
-#include <linux/ipv6_route.h>
-#include <linux/route.h>
-#include <netinet/in.h>
-#include <stdio.h>
-#include <string.h>
-#include <sys/ioctl.h>
-#include <sys/socket.h>
-#include <sys/stat.h>
-#include <sys/types.h>
-
-#include <log/log.h>
-
-#include "jni.h"
-#include <android-base/stringprintf.h>
-#include <android-base/unique_fd.h>
-#include <bpf/KernelUtils.h>
-#include <nativehelper/JNIHelp.h>
-#include <nativehelper/ScopedUtfChars.h>
-
-#ifndef IFF_NO_CARRIER
-#define IFF_NO_CARRIER 0x0040
-#endif
-
-namespace android {
-
-//------------------------------------------------------------------------------
-
-static void throwException(JNIEnv* env, int error, const char* action, const char* iface) {
- const std::string& msg = "Error: " + std::string(action) + " " + std::string(iface) + ": "
- + std::string(strerror(error));
- jniThrowException(env, "java/lang/IllegalStateException", msg.c_str());
-}
-
-// enable or disable carrier on tun / tap interface.
-static void setTunTapCarrierEnabledImpl(JNIEnv* env, const char* iface, int tunFd, bool enabled) {
- uint32_t carrierOn = enabled;
- if (ioctl(tunFd, TUNSETCARRIER, &carrierOn)) {
- throwException(env, errno, "set carrier", iface);
- }
-}
-
-static int createTunTapImpl(JNIEnv* env, bool isTun, bool hasCarrier, bool setIffMulticast,
- const char* iface) {
- base::unique_fd tun(open("/dev/tun", O_RDWR | O_NONBLOCK));
- ifreq ifr{};
-
- // Allocate interface.
- ifr.ifr_flags = (isTun ? IFF_TUN : IFF_TAP) | IFF_NO_PI;
- if (!hasCarrier) {
- // Using IFF_NO_CARRIER is supported starting in kernel version >= 6.0
- // Up until then, unsupported flags are ignored.
- if (!bpf::isAtLeastKernelVersion(6, 0, 0)) {
- throwException(env, EOPNOTSUPP, "IFF_NO_CARRIER not supported", ifr.ifr_name);
- return -1;
- }
- ifr.ifr_flags |= IFF_NO_CARRIER;
- }
- strlcpy(ifr.ifr_name, iface, IFNAMSIZ);
- if (ioctl(tun.get(), TUNSETIFF, &ifr)) {
- throwException(env, errno, "allocating", ifr.ifr_name);
- return -1;
- }
-
- // Mark some TAP interfaces as supporting multicast
- if (setIffMulticast && !isTun) {
- base::unique_fd inet6CtrlSock(socket(AF_INET6, SOCK_DGRAM, 0));
- ifr.ifr_flags = IFF_MULTICAST;
-
- if (ioctl(inet6CtrlSock.get(), SIOCSIFFLAGS, &ifr)) {
- throwException(env, errno, "set IFF_MULTICAST", ifr.ifr_name);
- return -1;
- }
- }
-
- return tun.release();
-}
-
-static void bringUpInterfaceImpl(JNIEnv* env, const char* iface) {
- // Activate interface using an unconnected datagram socket.
- base::unique_fd inet6CtrlSock(socket(AF_INET6, SOCK_DGRAM, 0));
-
- ifreq ifr{};
- strlcpy(ifr.ifr_name, iface, IFNAMSIZ);
- if (ioctl(inet6CtrlSock.get(), SIOCGIFFLAGS, &ifr)) {
- throwException(env, errno, "read flags", iface);
- return;
- }
- ifr.ifr_flags |= IFF_UP;
- if (ioctl(inet6CtrlSock.get(), SIOCSIFFLAGS, &ifr)) {
- throwException(env, errno, "set IFF_UP", iface);
- return;
- }
-}
-
-//------------------------------------------------------------------------------
-
-
-
-static void setTunTapCarrierEnabled(JNIEnv* env, jclass /* clazz */, jstring
- jIface, jint tunFd, jboolean enabled) {
- ScopedUtfChars iface(env, jIface);
- if (!iface.c_str()) {
- jniThrowNullPointerException(env, "iface");
- return;
- }
- setTunTapCarrierEnabledImpl(env, iface.c_str(), tunFd, enabled);
-}
-
-static jint createTunTap(JNIEnv* env, jclass /* clazz */, jboolean isTun,
- jboolean hasCarrier, jboolean setIffMulticast, jstring jIface) {
- ScopedUtfChars iface(env, jIface);
- if (!iface.c_str()) {
- jniThrowNullPointerException(env, "iface");
- return -1;
- }
-
- return createTunTapImpl(env, isTun, hasCarrier, setIffMulticast, iface.c_str());
-}
-
-static void bringUpInterface(JNIEnv* env, jclass /* clazz */, jstring jIface) {
- ScopedUtfChars iface(env, jIface);
- if (!iface.c_str()) {
- jniThrowNullPointerException(env, "iface");
- return;
- }
- bringUpInterfaceImpl(env, iface.c_str());
-}
-
-//------------------------------------------------------------------------------
-
-static const JNINativeMethod gMethods[] = {
- {"nativeSetTunTapCarrierEnabled", "(Ljava/lang/String;IZ)V", (void*)setTunTapCarrierEnabled},
- {"nativeCreateTunTap", "(ZZZLjava/lang/String;)I", (void*)createTunTap},
- {"nativeBringUpInterface", "(Ljava/lang/String;)V", (void*)bringUpInterface},
-};
-
-int register_com_android_server_TestNetworkService(JNIEnv* env) {
- return jniRegisterNativeMethods(env,
- "android/net/connectivity/com/android/server/TestNetworkService", gMethods,
- NELEM(gMethods));
-}
-
-}; // namespace android
diff --git a/service/jni/onload.cpp b/service/jni/onload.cpp
index 8e01260..f87470d 100644
--- a/service/jni/onload.cpp
+++ b/service/jni/onload.cpp
@@ -21,12 +21,11 @@
namespace android {
-int register_com_android_server_TestNetworkService(JNIEnv* env);
int register_com_android_server_connectivity_ClatCoordinator(JNIEnv* env);
int register_android_server_net_NetworkStatsFactory(JNIEnv* env);
int register_android_server_net_NetworkStatsService(JNIEnv* env);
int register_com_android_server_ServiceManagerWrapper(JNIEnv* env);
-int register_com_android_net_module_util_TimerFdUtils(JNIEnv *env,
+int register_com_android_net_module_util_ServiceConnectivityJni(JNIEnv *env,
char const *class_name);
extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
@@ -36,10 +35,6 @@
return JNI_ERR;
}
- if (register_com_android_server_TestNetworkService(env) < 0) {
- return JNI_ERR;
- }
-
if (register_com_android_server_ServiceManagerWrapper(env) < 0) {
return JNI_ERR;
}
@@ -58,9 +53,9 @@
}
}
- if (register_com_android_net_module_util_TimerFdUtils(
+ if (register_com_android_net_module_util_ServiceConnectivityJni(
env, "android/net/connectivity/com/android/net/module/util/"
- "TimerFdUtils") < 0) {
+ "ServiceConnectivityJni") < 0) {
return JNI_ERR;
}
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index 44868b2d..c743573 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -25,6 +25,8 @@
import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_MAP_PATH;
import static android.net.BpfNetMapsConstants.IIF_MATCH;
import static android.net.BpfNetMapsConstants.INGRESS_DISCARD_MAP_PATH;
+import static android.net.BpfNetMapsConstants.LOCAL_NET_ACCESS_MAP_PATH;
+import static android.net.BpfNetMapsConstants.LOCAL_NET_BLOCKED_UID_MAP_PATH;
import static android.net.BpfNetMapsConstants.LOCKDOWN_VPN_MATCH;
import static android.net.BpfNetMapsConstants.UID_OWNER_MAP_PATH;
import static android.net.BpfNetMapsConstants.UID_PERMISSION_MAP_PATH;
@@ -74,6 +76,7 @@
import com.android.net.module.util.IBpfMap;
import com.android.net.module.util.SingleWriterBpfMap;
import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Bool;
import com.android.net.module.util.Struct.S32;
import com.android.net.module.util.Struct.U32;
import com.android.net.module.util.Struct.U8;
@@ -81,6 +84,7 @@
import com.android.net.module.util.bpf.CookieTagMapValue;
import com.android.net.module.util.bpf.IngressDiscardKey;
import com.android.net.module.util.bpf.IngressDiscardValue;
+import com.android.net.module.util.bpf.LocalNetAccessKey;
import java.io.FileDescriptor;
import java.io.IOException;
@@ -131,6 +135,9 @@
private static IBpfMap<S32, U8> sDataSaverEnabledMap = null;
private static IBpfMap<IngressDiscardKey, IngressDiscardValue> sIngressDiscardMap = null;
+ private static IBpfMap<LocalNetAccessKey, Bool> sLocalNetAccessMap = null;
+ private static IBpfMap<U32, Bool> sLocalNetBlockedUidMap = null;
+
private static final List<Pair<Integer, String>> PERMISSION_LIST = Arrays.asList(
Pair.create(PERMISSION_INTERNET, "PERMISSION_INTERNET"),
Pair.create(PERMISSION_UPDATE_DEVICE_STATS, "PERMISSION_UPDATE_DEVICE_STATS")
@@ -186,6 +193,25 @@
sIngressDiscardMap = ingressDiscardMap;
}
+ /**
+ * Set localNetAccessMap for test.
+ */
+ @VisibleForTesting
+ public static void setLocalNetAccessMapForTest(
+ IBpfMap<LocalNetAccessKey, Bool> localNetAccessMap) {
+ sLocalNetAccessMap = localNetAccessMap;
+ }
+
+ /**
+ * Set localNetBlockedUidMap for test.
+ */
+ @VisibleForTesting
+ public static void setLocalNetBlockedUidMapForTest(
+ IBpfMap<U32, Bool> localNetBlockedUidMap) {
+ sLocalNetBlockedUidMap = localNetBlockedUidMap;
+ }
+
+
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private static IBpfMap<S32, U32> getConfigurationMap() {
try {
@@ -247,6 +273,26 @@
}
}
+ @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+ private static IBpfMap<U32, Bool> getLocalNetBlockedUidMap() {
+ try {
+ return SingleWriterBpfMap.getSingleton(LOCAL_NET_BLOCKED_UID_MAP_PATH,
+ U32.class, Bool.class);
+ } catch (ErrnoException e) {
+ throw new IllegalStateException("Cannot open local_net_blocked_uid map", e);
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+ private static IBpfMap<LocalNetAccessKey, Bool> getLocalNetAccessMap() {
+ try {
+ return SingleWriterBpfMap.getSingleton(LOCAL_NET_ACCESS_MAP_PATH,
+ LocalNetAccessKey.class, Bool.class);
+ } catch (ErrnoException e) {
+ throw new IllegalStateException("Cannot open local_net_access map", e);
+ }
+ }
+
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private static void initBpfMaps() {
if (sConfigurationMap == null) {
@@ -299,6 +345,27 @@
} catch (ErrnoException e) {
throw new IllegalStateException("Failed to initialize ingress discard map", e);
}
+
+ if (isAtLeast25Q2()) {
+ if (sLocalNetAccessMap == null) {
+ sLocalNetAccessMap = getLocalNetAccessMap();
+ }
+ try {
+ sLocalNetAccessMap.clear();
+ } catch (ErrnoException e) {
+ throw new IllegalStateException("Failed to initialize local_net_access map", e);
+ }
+
+ if (sLocalNetBlockedUidMap == null) {
+ sLocalNetBlockedUidMap = getLocalNetBlockedUidMap();
+ }
+ try {
+ sLocalNetBlockedUidMap.clear();
+ } catch (ErrnoException e) {
+ throw new IllegalStateException("Failed to initialize local_net_blocked_uid map",
+ e);
+ }
+ }
}
/**
@@ -387,6 +454,21 @@
}
}
+ private void throwIfPre25Q2(final String msg) {
+ if (!isAtLeast25Q2()) {
+ throw new UnsupportedOperationException(msg);
+ }
+ }
+
+ /*
+ ToDo : Remove this method when SdkLevel.isAtLeastB() is fixed, aosp is at sdk level 36 or use
+ NetworkStackUtils.isAtLeast25Q2 when it is moved to a static lib.
+ */
+ private static boolean isAtLeast25Q2() {
+ return SdkLevel.isAtLeastB() || (SdkLevel.isAtLeastV()
+ && "Baklava".equals(Build.VERSION.CODENAME));
+ }
+
private void removeRule(final int uid, final long match, final String caller) {
try {
synchronized (sUidOwnerMap) {
@@ -810,6 +892,113 @@
}
/**
+ * Add configuration to local_net_access trie map.
+ * @param lpmBitlen prefix length that will be used for longest matching
+ * @param iface interface name
+ * @param address remote address. ipv4 addresses would be mapped to v6
+ * @param protocol required for longest match in special cases
+ * @param remotePort src/dst port for ingress/egress
+ * @param isAllowed is the local network call allowed or blocked.
+ */
+ @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+ public void addLocalNetAccess(final int lpmBitlen, final String iface,
+ final InetAddress address, final int protocol, final int remotePort,
+ final boolean isAllowed) {
+ throwIfPre25Q2("addLocalNetAccess is not available on pre-B devices");
+ final int ifIndex = mDeps.getIfIndex(iface);
+ if (ifIndex == 0) {
+ Log.e(TAG, "Failed to get if index, skip addLocalNetAccess for " + address
+ + "(" + iface + ")");
+ return;
+ }
+ LocalNetAccessKey localNetAccessKey = new LocalNetAccessKey(lpmBitlen, ifIndex,
+ address, protocol, remotePort);
+
+ try {
+ sLocalNetAccessMap.updateEntry(localNetAccessKey, new Bool(isAllowed));
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to add local network access for localNetAccessKey : "
+ + localNetAccessKey + ", isAllowed : " + isAllowed);
+ }
+ }
+
+ /**
+ * False if the configuration is disallowed.
+ *
+ * @param lpmBitlen prefix length that will be used for longest matching
+ * @param iface interface name
+ * @param address remote address. ipv4 addresses would be mapped to v6
+ * @param protocol required for longest match in special cases
+ * @param remotePort src/dst port for ingress/egress
+ */
+ @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+ public boolean getLocalNetAccess(final int lpmBitlen, final String iface,
+ final InetAddress address, final int protocol, final int remotePort) {
+ throwIfPre25Q2("getLocalNetAccess is not available on pre-B devices");
+ final int ifIndex = mDeps.getIfIndex(iface);
+ if (ifIndex == 0) {
+ Log.e(TAG, "Failed to get if index, returning default from getLocalNetAccess for "
+ + address + "(" + iface + ")");
+ return true;
+ }
+ LocalNetAccessKey localNetAccessKey = new LocalNetAccessKey(lpmBitlen, ifIndex,
+ address, protocol, remotePort);
+ try {
+ Bool value = sLocalNetAccessMap.getValue(localNetAccessKey);
+ return value == null ? true : value.val;
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to find local network access configuration for "
+ + "localNetAccessKey : " + localNetAccessKey);
+ }
+ return true;
+ }
+
+ /**
+ * Add uid to local_net_blocked_uid map.
+ * @param uid application uid that needs to block local network calls.
+ */
+ @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+ public void addUidToLocalNetBlockMap(final int uid) {
+ throwIfPre25Q2("addUidToLocalNetBlockMap is not available on pre-B devices");
+ try {
+ sLocalNetBlockedUidMap.updateEntry(new U32(uid), new Bool(true));
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to add local network blocked for uid : " + uid);
+ }
+ }
+
+ /**
+ * True if local network calls are blocked for application.
+ * @param uid application uid that needs check if local network calls are blocked.
+ */
+ @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+ public boolean getUidValueFromLocalNetBlockMap(final int uid) {
+ throwIfPre25Q2("getUidValueFromLocalNetBlockMap is not available on pre-B devices");
+ try {
+ Bool value = sLocalNetBlockedUidMap.getValue(new U32(uid));
+ return value == null ? false : value.val;
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to find uid(" + uid
+ + ") is present in local network blocked map");
+ }
+ return false;
+ }
+
+ /**
+ * Remove uid from local_net_blocked_uid map(if present).
+ * @param uid application uid that needs check if local network calls are blocked.
+ */
+ @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+ public void removeUidFromLocalNetBlockMap(final int uid) {
+ throwIfPre25Q2("removeUidFromLocalNetBlockMap is not available on pre-B devices");
+ try {
+ sLocalNetBlockedUidMap.deleteEntry(new U32(uid));
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to remove uid(" + uid + ") from local network blocked map");
+ }
+ }
+
+ /**
* Get granted permissions for specified uid. If uid is not in the map, this method returns
* {@link android.net.INetd.PERMISSION_INTERNET} since this is a default permission.
* See {@link #setNetPermForUids}
@@ -1079,6 +1268,14 @@
(key, value) -> "[" + key.dstAddr + "]: "
+ value.iif1 + "(" + mDeps.getIfName(value.iif1) + "), "
+ value.iif2 + "(" + mDeps.getIfName(value.iif2) + ")");
+ if (sLocalNetBlockedUidMap != null) {
+ BpfDump.dumpMap(sLocalNetAccessMap, pw, "sLocalNetAccessMap",
+ (key, value) -> "[" + key + "]: " + value);
+ }
+ if (sLocalNetBlockedUidMap != null) {
+ BpfDump.dumpMap(sLocalNetBlockedUidMap, pw, "sLocalNetBlockedUidMap",
+ (key, value) -> "[" + key + "]: " + value);
+ }
dumpDataSaverConfig(pw);
pw.decreaseIndent();
}
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index fe26858..18801f0 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -110,6 +110,7 @@
import static android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS;
import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS;
import static android.net.NetworkCapabilities.RES_ID_UNSET;
+import static android.net.NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
@@ -6765,7 +6766,7 @@
final NetworkOfferInfo offer =
findNetworkOfferInfoByCallback((INetworkOfferCallback) msg.obj);
if (null != offer) {
- handleUnregisterNetworkOffer(offer);
+ handleUnregisterNetworkOffer(offer, true /* releaseReservations */);
}
break;
}
@@ -7682,17 +7683,23 @@
}
}
- private void ensureAllNetworkRequestsHaveType(List<NetworkRequest> requests) {
+ private void ensureAllNetworkRequestsHaveSupportedType(List<NetworkRequest> requests) {
+ final boolean isMultilayerRequest = requests.size() > 1;
for (int i = 0; i < requests.size(); i++) {
- ensureNetworkRequestHasType(requests.get(i));
+ ensureNetworkRequestHasSupportedType(requests.get(i), isMultilayerRequest);
}
}
- private void ensureNetworkRequestHasType(NetworkRequest request) {
+ private void ensureNetworkRequestHasSupportedType(NetworkRequest request,
+ boolean isMultilayerRequest) {
if (request.type == NetworkRequest.Type.NONE) {
throw new IllegalArgumentException(
"All NetworkRequests in ConnectivityService must have a type");
}
+ if (isMultilayerRequest && request.type == NetworkRequest.Type.RESERVATION) {
+ throw new IllegalArgumentException(
+ "Reservation requests are not supported in multilayer request");
+ }
}
/**
@@ -7844,7 +7851,7 @@
NetworkRequestInfo(int asUid, @NonNull final List<NetworkRequest> r,
@NonNull final NetworkRequest requestForCallback, @Nullable final PendingIntent pi,
@Nullable String callingAttributionTag, final int preferenceOrder) {
- ensureAllNetworkRequestsHaveType(r);
+ ensureAllNetworkRequestsHaveSupportedType(r);
mRequests = initializeRequests(r);
mNetworkRequestForCallback = requestForCallback;
mPendingIntent = pi;
@@ -7878,7 +7885,7 @@
@NetworkCallback.Flag int callbackFlags,
@Nullable String callingAttributionTag, int declaredMethodsFlags) {
super();
- ensureAllNetworkRequestsHaveType(r);
+ ensureAllNetworkRequestsHaveSupportedType(r);
mRequests = initializeRequests(r);
mNetworkRequestForCallback = requestForCallback;
mMessenger = m;
@@ -7898,7 +7905,7 @@
NetworkRequestInfo(@NonNull final NetworkRequestInfo nri,
@NonNull final List<NetworkRequest> r) {
super();
- ensureAllNetworkRequestsHaveType(r);
+ ensureAllNetworkRequestsHaveSupportedType(r);
mRequests = initializeRequests(r);
mNetworkRequestForCallback = nri.getNetworkRequestForCallback();
final NetworkAgentInfo satisfier = nri.getSatisfier();
@@ -8887,7 +8894,7 @@
@Override
public void releaseNetworkRequest(NetworkRequest networkRequest) {
- ensureNetworkRequestHasType(networkRequest);
+ ensureNetworkRequestHasSupportedType(networkRequest, false /* isMultilayerRequest */);
mHandler.sendMessage(mHandler.obtainMessage(
EVENT_RELEASE_NETWORK_REQUEST, mDeps.getCallingUid(), 0, networkRequest));
}
@@ -8930,6 +8937,11 @@
Objects.requireNonNull(score);
Objects.requireNonNull(caps);
Objects.requireNonNull(callback);
+ if (caps.hasTransport(TRANSPORT_TEST)) {
+ enforceAnyPermissionOf(mContext, Manifest.permission.MANAGE_TEST_NETWORKS);
+ } else {
+ enforceNetworkFactoryPermission();
+ }
final boolean yieldToBadWiFi = caps.hasTransport(TRANSPORT_CELLULAR) && !avoidBadWifi();
final NetworkOffer offer = new NetworkOffer(
FullScore.makeProspectiveScore(score, caps, yieldToBadWiFi),
@@ -8968,7 +8980,7 @@
}
}
for (final NetworkOfferInfo noi : toRemove) {
- handleUnregisterNetworkOffer(noi);
+ handleUnregisterNetworkOffer(noi, true /* releaseReservations */);
}
if (DBG) log("unregisterNetworkProvider for " + npi.name);
}
@@ -9401,7 +9413,7 @@
@Override
public void binderDied() {
- mHandler.post(() -> handleUnregisterNetworkOffer(this));
+ mHandler.post(() -> handleUnregisterNetworkOffer(this, true /* releaseReservations */));
}
}
@@ -9440,41 +9452,61 @@
return;
}
final NetworkOfferInfo existingOffer = findNetworkOfferInfoByCallback(newOffer.callback);
+
+ // If a reserved offer is updated, ensure the capabilities are not changed. This ensures
+ // that the reserved offer's capabilities match the ones passed by the onReserved callback,
+ // which is sent only once.
+ //
+ // TODO: consider letting the provider change the capabilities of an offer as long as they
+ // continue to satisfy the capabilities that were passed to onReserved. This is not needed
+ // today, but it shouldn't violate the API contract:
+ // - NetworkOffer capabilities are not promises
+ // - The app making a reservation must never assume that the capabilities of the reserved
+ // network are equal to the ones that were passed to onReserved. There will almost always be
+ // other capabilities, for example, those that change at runtime such as VALIDATED or
+ // NOT_SUSPENDED.
+ if (null != existingOffer
+ && existingOffer.offer.caps.getReservationId() != RES_ID_UNSET
+ && existingOffer.offer.caps.getReservationId() != RES_ID_MATCH_ALL_RESERVATIONS
+ && !newOffer.caps.equals(existingOffer.offer.caps)) {
+ // Reserved offers are not allowed to update their NetworkCapabilities.
+ // Doing so will immediately remove the offer from CS and send onUnavailable to the app.
+ handleUnregisterNetworkOffer(existingOffer, true /* releaseReservations */);
+ existingOffer.offer.notifyUnneeded();
+ logwtf("Reserved offers must never update their reserved NetworkCapabilities");
+ return;
+ }
+
+ final NetworkOfferInfo noi = new NetworkOfferInfo(newOffer);
if (null != existingOffer) {
- // TODO: to support updating the score for reserved offers by calling
- // ConnectivityManager#offerNetwork with the same callback object or via
- // updateOfferScore, prevent handleUnregisterNetworkOffer() from sending an
- // onUnavailable() callback here.
- handleUnregisterNetworkOffer(existingOffer);
+ // Do not send onUnavailable for a reserved offer when updating it.
+ handleUnregisterNetworkOffer(existingOffer, false /* releaseReservations */);
newOffer.migrateFrom(existingOffer.offer);
if (DBG) {
// handleUnregisterNetworkOffer has already logged the old offer
log("update offer from providerId " + newOffer.providerId + " new : " + newOffer);
}
} else {
+ final NetworkRequestInfo reservationNri = maybeGetNriForReservedOffer(noi);
+ if (reservationNri != null) {
+ // A NetworkRequest is only allowed to trigger a single reserved offer (and
+ // onReserved() callback). All subsequent offers are ignored. This either indicates
+ // a bug in the provider (e.g., responding twice to the same reservation, or
+ // updating the capabilities of a reserved offer), or multiple providers responding
+ // to the same offer (which could happen, but is not useful to the requesting app).
+ if (reservationNri.getReservedCapabilities() != null) {
+ loge("A reservation can only trigger a single offer; new offer is ignored.");
+ return;
+ }
+ // Always update the reserved offer before calling callCallbackForRequest.
+ reservationNri.setReservedCapabilities(noi.offer.caps);
+ callCallbackForRequest(
+ reservationNri, null /*networkAgent*/, CALLBACK_RESERVED, 0 /*arg1*/);
+ }
if (DBG) {
log("register offer from providerId " + newOffer.providerId + " : " + newOffer);
}
}
- final NetworkOfferInfo noi = new NetworkOfferInfo(newOffer);
- final NetworkRequestInfo reservationNri = maybeGetNriForReservedOffer(noi);
- if (reservationNri != null) {
- // A NetworkRequest is only allowed to trigger a single reserved offer (and onReserved()
- // callback). All subsequent offers are ignored. This either indicates a bug in the
- // provider (e.g., responding twice to the same reservation, or updating the
- // capabilities of a reserved offer), or multiple providers responding to the same offer
- // (which could happen, but is not useful to the requesting app).
- // TODO: add proper support for offer migration; i.e. allow the score of a reservation
- // offer to be updated.
- if (reservationNri.getReservedCapabilities() != null) {
- loge("A reservation can only trigger a single offer; new offer is ignored.");
- return;
- }
- // Always update the reserved offer before calling callCallbackForRequest.
- reservationNri.setReservedCapabilities(noi.offer.caps);
- callCallbackForRequest(
- reservationNri, null /* networkAgent */, CALLBACK_RESERVED, 0 /* arg1 */);
- }
try {
noi.offer.callback.asBinder().linkToDeath(noi, 0 /* flags */);
@@ -9486,7 +9518,8 @@
issueNetworkNeeds(noi);
}
- private void handleUnregisterNetworkOffer(@NonNull final NetworkOfferInfo noi) {
+ private void handleUnregisterNetworkOffer(@NonNull final NetworkOfferInfo noi,
+ boolean releaseReservations) {
ensureRunningOnConnectivityServiceThread();
if (DBG) {
log("unregister offer from providerId " + noi.offer.providerId + " : " + noi.offer);
@@ -9504,11 +9537,10 @@
// handleRegisterNetworkOffer() in the case of a migration (which would be ignored as it
// follows an onUnavailable).
final NetworkRequestInfo nri = maybeGetNriForReservedOffer(noi);
- if (nri != null) {
+ if (releaseReservations && nri != null) {
handleRemoveNetworkRequest(nri);
callCallbackForRequest(nri, null /* networkAgent */, CALLBACK_UNAVAIL, 0 /* arg1 */);
}
-
noi.offer.callback.asBinder().unlinkToDeath(noi, 0 /* flags */);
}
diff --git a/service/src/com/android/server/L2capNetworkProvider.java b/service/src/com/android/server/L2capNetworkProvider.java
new file mode 100644
index 0000000..c5ec9ee
--- /dev/null
+++ b/service/src/com/android/server/L2capNetworkProvider.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import static android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_6LOWPAN;
+import static android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_ANY;
+import static android.net.L2capNetworkSpecifier.PSM_ANY;
+import static android.net.L2capNetworkSpecifier.ROLE_SERVER;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
+import static android.net.NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS;
+import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
+import static android.content.pm.PackageManager.FEATURE_BLUETOOTH_LE;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.L2capNetworkSpecifier;
+import android.net.NetworkCapabilities;
+import android.net.NetworkProvider;
+import android.net.NetworkProvider.NetworkOfferCallback;
+import android.net.NetworkRequest;
+import android.net.NetworkScore;
+import android.net.NetworkSpecifier;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Map;
+
+
+public class L2capNetworkProvider {
+ private static final String TAG = L2capNetworkProvider.class.getSimpleName();
+ private final Dependencies mDeps;
+ private final Context mContext;
+ private final HandlerThread mHandlerThread;
+ private final Handler mHandler;
+ private final NetworkProvider mProvider;
+ private final BlanketReservationOffer mBlanketOffer;
+ private final Map<Integer, ReservedServerOffer> mReservedServerOffers = new ArrayMap<>();
+
+ /**
+ * The blanket reservation offer is used to create an L2CAP server network, i.e. a network
+ * based on a BluetoothServerSocket.
+ *
+ * Note that NetworkCapabilities matching semantics will cause onNetworkNeeded to be called for
+ * requests that do not have a NetworkSpecifier set.
+ */
+ private class BlanketReservationOffer implements NetworkOfferCallback {
+ // TODO: ensure that once the incoming request is satisfied, the blanket offer does not get
+ // unneeded. This means the blanket offer must always outscore the reserved offer. This
+ // might require setting the blanket offer as setTransportPrimary().
+ public static final NetworkScore SCORE = new NetworkScore.Builder().build();
+ // Note the missing NET_CAPABILITY_NOT_RESTRICTED marking the network as restricted.
+ public static final NetworkCapabilities CAPABILITIES;
+ static {
+ final L2capNetworkSpecifier l2capNetworkSpecifier = new L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .build();
+ NetworkCapabilities caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
+ .addTransportType(TRANSPORT_BLUETOOTH)
+ // TODO: consider removing NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED.
+ .addCapability(NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED)
+ .addCapability(NET_CAPABILITY_NOT_CONGESTED)
+ .addCapability(NET_CAPABILITY_NOT_METERED)
+ .addCapability(NET_CAPABILITY_NOT_ROAMING)
+ .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .addCapability(NET_CAPABILITY_NOT_VPN)
+ .setNetworkSpecifier(l2capNetworkSpecifier)
+ .build();
+ caps.setReservationId(RES_ID_MATCH_ALL_RESERVATIONS);
+ CAPABILITIES = caps;
+ }
+
+ // TODO: consider moving this into L2capNetworkSpecifier as #isValidServerReservation().
+ private boolean isValidL2capSpecifier(@Nullable NetworkSpecifier spec) {
+ if (spec == null) return false;
+ // If spec is not null, L2capNetworkSpecifier#canBeSatisfiedBy() guarantees the
+ // specifier is of type L2capNetworkSpecifier.
+ final L2capNetworkSpecifier l2capSpec = (L2capNetworkSpecifier) spec;
+
+ // The ROLE_SERVER offer can be satisfied by a ROLE_ANY request.
+ if (l2capSpec.getRole() != ROLE_SERVER) return false;
+
+ // HEADER_COMPRESSION_ANY is never valid in a request.
+ if (l2capSpec.getHeaderCompression() == HEADER_COMPRESSION_ANY) return false;
+
+ // remoteAddr must be null for ROLE_SERVER requests.
+ if (l2capSpec.getRemoteAddress() != null) return false;
+
+ // reservation must allocate a PSM, so only PSM_ANY can be passed.
+ if (l2capSpec.getPsm() != PSM_ANY) return false;
+
+ return true;
+ }
+
+ @Override
+ public void onNetworkNeeded(NetworkRequest request) {
+ Log.d(TAG, "New reservation request: " + request);
+ if (!isValidL2capSpecifier(request.getNetworkSpecifier())) {
+ Log.w(TAG, "Ignoring invalid reservation request: " + request);
+ return;
+ }
+
+ final NetworkCapabilities reservationCaps = request.networkCapabilities;
+ final ReservedServerOffer reservedOffer = new ReservedServerOffer(reservationCaps);
+
+ final NetworkCapabilities reservedCaps = reservedOffer.getReservedCapabilities();
+ mProvider.registerNetworkOffer(SCORE, reservedCaps, mHandler::post, reservedOffer);
+ mReservedServerOffers.put(request.requestId, reservedOffer);
+ }
+
+ @Override
+ public void onNetworkUnneeded(NetworkRequest request) {
+ if (!mReservedServerOffers.containsKey(request.requestId)) {
+ return;
+ }
+
+ final ReservedServerOffer reservedOffer = mReservedServerOffers.get(request.requestId);
+ // Note that the reserved offer gets torn down when the reservation goes away, even if
+ // there are lingering requests.
+ reservedOffer.tearDown();
+ mProvider.unregisterNetworkOffer(reservedOffer);
+ }
+ }
+
+ private class ReservedServerOffer implements NetworkOfferCallback {
+ private final boolean mUseHeaderCompression;
+ private final int mPsm;
+ private final NetworkCapabilities mReservedCapabilities;
+
+ public ReservedServerOffer(NetworkCapabilities reservationCaps) {
+ // getNetworkSpecifier() is guaranteed to return a non-null L2capNetworkSpecifier.
+ final L2capNetworkSpecifier reservationSpec =
+ (L2capNetworkSpecifier) reservationCaps.getNetworkSpecifier();
+ mUseHeaderCompression =
+ reservationSpec.getHeaderCompression() == HEADER_COMPRESSION_6LOWPAN;
+
+ // TODO: open BluetoothServerSocket and allocate a PSM.
+ mPsm = 0x80;
+
+ final L2capNetworkSpecifier reservedSpec = new L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(reservationSpec.getHeaderCompression())
+ .setPsm(mPsm)
+ .build();
+ mReservedCapabilities = new NetworkCapabilities.Builder(reservationCaps)
+ .setNetworkSpecifier(reservedSpec)
+ .build();
+ }
+
+ public NetworkCapabilities getReservedCapabilities() {
+ return mReservedCapabilities;
+ }
+
+ @Override
+ public void onNetworkNeeded(NetworkRequest request) {
+ // TODO: implement
+ }
+
+ @Override
+ public void onNetworkUnneeded(NetworkRequest request) {
+ // TODO: implement
+ }
+
+ /**
+ * Called when the reservation goes away and the reserved offer must be torn down.
+ *
+ * This method can be called multiple times.
+ */
+ public void tearDown() {
+ // TODO: implement.
+ // This method can be called multiple times.
+ }
+ }
+
+ @VisibleForTesting
+ public static class Dependencies {
+ /** Get NetworkProvider */
+ public NetworkProvider getNetworkProvider(Context context, Looper looper) {
+ return new NetworkProvider(context, looper, TAG);
+ }
+
+ /** Get the HandlerThread for L2capNetworkProvider to run on */
+ public HandlerThread getHandlerThread() {
+ final HandlerThread thread = new HandlerThread("L2capNetworkProviderThread");
+ thread.start();
+ return thread;
+ }
+ }
+
+ public L2capNetworkProvider(Context context) {
+ this(new Dependencies(), context);
+ }
+
+ @VisibleForTesting
+ public L2capNetworkProvider(Dependencies deps, Context context) {
+ mDeps = deps;
+ mContext = context;
+ mHandlerThread = mDeps.getHandlerThread();
+ mHandler = new Handler(mHandlerThread.getLooper());
+ mProvider = mDeps.getNetworkProvider(context, mHandlerThread.getLooper());
+ mBlanketOffer = new BlanketReservationOffer();
+ }
+
+ /**
+ * Start L2capNetworkProvider.
+ *
+ * Called on CS Handler thread.
+ */
+ public void start() {
+ final PackageManager pm = mContext.getPackageManager();
+ if (pm.hasSystemFeature(FEATURE_BLUETOOTH_LE)) {
+ mContext.getSystemService(ConnectivityManager.class).registerNetworkProvider(mProvider);
+ mProvider.registerNetworkOffer(BlanketReservationOffer.SCORE,
+ BlanketReservationOffer.CAPABILITIES, mHandler::post, mBlanketOffer);
+ }
+ }
+}
diff --git a/service/src/com/android/server/TestNetworkService.java b/service/src/com/android/server/TestNetworkService.java
index 4d39d7d..96f4e20 100644
--- a/service/src/com/android/server/TestNetworkService.java
+++ b/service/src/com/android/server/TestNetworkService.java
@@ -48,6 +48,7 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.NetworkStackConstants;
+import com.android.net.module.util.ServiceConnectivityJni;
import java.io.IOException;
import java.io.UncheckedIOException;
@@ -75,15 +76,6 @@
@NonNull private final ConnectivityManager mCm;
@NonNull private final NetworkProvider mNetworkProvider;
- // Native method stubs
- private static native int nativeCreateTunTap(boolean isTun, boolean hasCarrier,
- boolean setIffMulticast, @NonNull String iface);
-
- private static native void nativeSetTunTapCarrierEnabled(@NonNull String iface, int tunFd,
- boolean enabled);
-
- private static native void nativeBringUpInterface(String iface);
-
@VisibleForTesting
protected TestNetworkService(@NonNull Context context) {
mHandlerThread = new HandlerThread("TestNetworkServiceThread");
@@ -143,7 +135,8 @@
// flags atomically.
final boolean setIffMulticast = bringUp;
ParcelFileDescriptor tunIntf = ParcelFileDescriptor.adoptFd(
- nativeCreateTunTap(isTun, hasCarrier, setIffMulticast, interfaceName));
+ ServiceConnectivityJni.createTunTap(
+ isTun, hasCarrier, setIffMulticast, interfaceName));
// Disable DAD and remove router_solicitation_delay before assigning link addresses.
if (disableIpv6ProvisioningDelay) {
@@ -160,7 +153,7 @@
}
if (bringUp) {
- nativeBringUpInterface(interfaceName);
+ ServiceConnectivityJni.bringUpInterface(interfaceName);
}
return new TestNetworkInterface(tunIntf, interfaceName);
@@ -403,11 +396,11 @@
@Override
public void setCarrierEnabled(@NonNull TestNetworkInterface iface, boolean enabled) {
enforceTestNetworkPermissions(mContext);
- nativeSetTunTapCarrierEnabled(iface.getInterfaceName(), iface.getFileDescriptor().getFd(),
- enabled);
+ ServiceConnectivityJni.setTunTapCarrierEnabled(iface.getInterfaceName(),
+ iface.getFileDescriptor().getFd(), enabled);
// Explicitly close fd after use to prevent StrictMode from complaining.
// Also, explicitly referencing iface guarantees that the object is not garbage collected
- // before nativeSetTunTapCarrierEnabled() executes.
+ // before setTunTapCarrierEnabled() executes.
try {
iface.getFileDescriptor().close();
} catch (IOException e) {
diff --git a/service/src/com/android/server/connectivity/NetworkOffer.java b/service/src/com/android/server/connectivity/NetworkOffer.java
index eea382e..d294046 100644
--- a/service/src/com/android/server/connectivity/NetworkOffer.java
+++ b/service/src/com/android/server/connectivity/NetworkOffer.java
@@ -42,6 +42,7 @@
* @hide
*/
public class NetworkOffer implements NetworkRanker.Scoreable {
+ private static final String TAG = NetworkOffer.class.getSimpleName();
@NonNull public final FullScore score;
@NonNull public final NetworkCapabilities caps;
@NonNull public final INetworkOfferCallback callback;
@@ -126,6 +127,23 @@
}
/**
+ * Sends onNetworkUnneeded for any remaining NetworkRequests.
+ *
+ * Used after a NetworkOffer migration failed to let the provider know that its networks should
+ * be torn down (as the offer is no longer registered).
+ */
+ public void notifyUnneeded() {
+ try {
+ for (NetworkRequest request : mCurrentlyNeeded) {
+ callback.onNetworkUnneeded(request);
+ }
+ } catch (RemoteException e) {
+ // The remote is dead; nothing to do.
+ }
+ mCurrentlyNeeded.clear();
+ }
+
+ /**
* Migrate from, and take over, a previous offer.
*
* When an updated offer is sent from a provider, call this method on the new offer, passing
diff --git a/service/src/com/android/server/connectivity/PermissionMonitor.java b/service/src/com/android/server/connectivity/PermissionMonitor.java
index beaa174..0d388e8 100755
--- a/service/src/com/android/server/connectivity/PermissionMonitor.java
+++ b/service/src/com/android/server/connectivity/PermissionMonitor.java
@@ -31,6 +31,7 @@
import static android.net.INetd.PERMISSION_UNINSTALLED;
import static android.net.INetd.PERMISSION_UPDATE_DEVICE_STATS;
import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
+import static android.net.connectivity.ConnectivityCompatChanges.RESTRICT_LOCAL_NETWORK;
import static android.os.Process.INVALID_UID;
import static android.os.Process.SYSTEM_UID;
@@ -38,6 +39,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.compat.CompatChanges;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -70,6 +72,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import com.android.modules.utils.build.SdkLevel;
+import com.android.net.flags.Flags;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.SharedLog;
import com.android.networkstack.apishim.ProcessShimImpl;
@@ -279,6 +282,12 @@
mContext = context;
mBpfNetMaps = bpfNetMaps;
mThread = thread;
+ if (Flags.restrictLocalNetwork()) {
+ // This listener should finish registration by the time the system has completed
+ // boot setup such that any changes to runtime permissions for local network
+ // restrictions can only occur after this registration has completed.
+ mPackageManager.addOnPermissionsChangeListener(new PermissionChangeListener());
+ }
}
private void ensureRunningOnHandlerThread() {
@@ -1311,4 +1320,19 @@
private static void loge(String s, Throwable e) {
Log.e(TAG, s, e);
}
+
+ private class PermissionChangeListener implements PackageManager.OnPermissionsChangedListener {
+ @Override
+ public void onPermissionsChanged(int uid) {
+ // RESTRICT_LOCAL_NETWORK is a compat change that is enabled when developers manually
+ // opt-in to this change, or when the app's targetSdkVersion is greater than 36.
+ // The RESTRICT_LOCAL_NETWORK compat change is used here instead of the
+ // Flags.restrictLocalNetwork() is used to offer the feature to devices, but it will
+ // only be enforced when develoeprs choose to enable it.
+ // TODO(b/394567896): Update compat change checks
+ if (CompatChanges.isChangeEnabled(RESTRICT_LOCAL_NETWORK, uid)) {
+ // TODO(b/388803658): Update network permissions and record change
+ }
+ }
+ }
}
diff --git a/service/src/com/android/server/net/L2capPacketForwarder.java b/service/src/com/android/server/net/L2capPacketForwarder.java
new file mode 100644
index 0000000..cef351c
--- /dev/null
+++ b/service/src/com/android/server/net/L2capPacketForwarder.java
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import android.bluetooth.BluetoothSocket;
+import android.os.Handler;
+import android.os.ParcelFileDescriptor;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Forwards packets from a BluetoothSocket of type L2CAP to a tun fd and vice versa.
+ *
+ * The forwarding logic operates on raw IP packets and there are no ethernet headers.
+ * Therefore, L3 MTU = L2 MTU.
+ */
+public class L2capPacketForwarder {
+ private static final String TAG = "L2capPacketForwarder";
+
+ // DCT specifies an MTU of 1500.
+ // TODO: Set /proc/sys/net/ipv6/conf/${iface}/mtu to 1280 and the link MTU to 1528 to accept
+ // slightly larger packets on ingress (i.e. packets passing through a NAT64 gateway).
+ // MTU determines the value of the read buffers, so use the larger of the two.
+ @VisibleForTesting
+ public static final int MTU = 1528;
+ private final Handler mHandler;
+ private final IReadWriteFd mTunFd;
+ private final IReadWriteFd mL2capFd;
+ private final L2capThread mIngressThread;
+ private final L2capThread mEgressThread;
+ private final ICallback mCallback;
+
+ public interface ICallback {
+ /** Called when an error is encountered; should tear down forwarding. */
+ void onError();
+ }
+
+ private interface IReadWriteFd {
+ /**
+ * Read up to len bytes into bytes[off] and return bytes actually read.
+ *
+ * bytes[] must be of size >= off + len.
+ */
+ int read(byte[] bytes, int off, int len) throws IOException;
+ /**
+ * Write len bytes starting from bytes[off]
+ *
+ * bytes[] must be of size >= off + len.
+ */
+ void write(byte[] bytes, int off, int len) throws IOException;
+ /** Disallow further receptions, shutdown(fd, SHUT_RD) */
+ void shutdownRead();
+ /** Disallow further transmissions, shutdown(fd, SHUT_WR) */
+ void shutdownWrite();
+ /** Close the fd */
+ void close();
+ }
+
+ @VisibleForTesting
+ public static class BluetoothSocketWrapper implements IReadWriteFd {
+ private final BluetoothSocket mSocket;
+ private final InputStream mInputStream;
+ private final OutputStream mOutputStream;
+
+ public BluetoothSocketWrapper(BluetoothSocket socket) {
+ // TODO: assert that MTU can fit within Bluetooth L2CAP SDU (maximum size of an L2CAP
+ // packet). The L2CAP SDU is 65535 by default, but can be less when using hardware
+ // offload.
+ mSocket = socket;
+ try {
+ mInputStream = socket.getInputStream();
+ mOutputStream = socket.getOutputStream();
+ } catch (IOException e) {
+ // Per the API docs, this should not actually be possible.
+ Log.wtf(TAG, "Failed to get Input/OutputStream", e);
+ // Fail hard.
+ throw new IllegalStateException("Failed to get Input/OutputStream");
+ }
+ }
+
+ /** Read from the BluetoothSocket. */
+ @Override
+ public int read(byte[] bytes, int off, int len) throws IOException {
+ // Note: EINTR is handled internally and automatically triggers a retry loop.
+ int bytesRead = mInputStream.read(bytes, off, len);
+ if (bytesRead > MTU) {
+ // Don't try to recover, just trigger network teardown. This might indicate a bug in
+ // the Bluetooth stack.
+ throw new IOException("Packet exceeds MTU");
+ }
+ return bytesRead;
+ }
+
+ /** Write to the BluetoothSocket. */
+ @Override
+ public void write(byte[] bytes, int off, int len) throws IOException {
+ // Note: EINTR is handled internally and automatically triggers a retry loop.
+ mOutputStream.write(bytes, off, len);
+ }
+
+ @Override
+ public void shutdownRead() {
+ // BluetoothSocket does not expose methods to shutdown read / write individually;
+ // however, BluetoothSocket#close() shuts down both read and write and is safe to be
+ // called multiple times from any thread.
+ try {
+ mSocket.close();
+ } catch (IOException e) {
+ Log.w(TAG, "shutdownRead: Failed to close BluetoothSocket", e);
+ }
+ }
+
+ @Override
+ public void shutdownWrite() {
+ // BluetoothSocket does not expose methods to shutdown read / write individually;
+ // however, BluetoothSocket#close() shuts down both read and write and is safe to be
+ // called multiple times from any thread.
+ try {
+ mSocket.close();
+ } catch (IOException e) {
+ Log.w(TAG, "shutdownWrite: Failed to close BluetoothSocket", e);
+ }
+ }
+
+ @Override
+ public void close() {
+ // BluetoothSocket#close() is safe to be called multiple times.
+ try {
+ mSocket.close();
+ } catch (IOException e) {
+ Log.w(TAG, "close: Failed to close BluetoothSocket", e);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public static class FdWrapper implements IReadWriteFd {
+ private final ParcelFileDescriptor mFd;
+
+ public FdWrapper(ParcelFileDescriptor fd) {
+ mFd = fd;
+ }
+
+ @Override
+ public int read(byte[] bytes, int off, int len) throws IOException {
+ try {
+ // Note: EINTR is handled internally and automatically triggers a retry loop.
+ return Os.read(mFd.getFileDescriptor(), bytes, off, len);
+ } catch (ErrnoException e) {
+ throw new IOException(e);
+ }
+ }
+
+ /**
+ * Write to BluetoothSocket.
+ */
+ @Override
+ public void write(byte[] bytes, int off, int len) throws IOException {
+ try {
+ // Note: EINTR is handled internally and automatically triggers a retry loop.
+ Os.write(mFd.getFileDescriptor(), bytes, off, len);
+ } catch (ErrnoException e) {
+ throw new IOException(e);
+ }
+ }
+
+ @Override
+ public void shutdownRead() {
+ try {
+ Os.shutdown(mFd.getFileDescriptor(), OsConstants.SHUT_RD);
+ } catch (ErrnoException e) {
+ Log.w(TAG, "shutdownRead: Failed to shutdown(fd, SHUT_RD)", e);
+ }
+ }
+
+ @Override
+ public void shutdownWrite() {
+ try {
+ Os.shutdown(mFd.getFileDescriptor(), OsConstants.SHUT_WR);
+ } catch (ErrnoException e) {
+ Log.w(TAG, "shutdownWrite: Failed to shutdown(fd, SHUT_WR)", e);
+ }
+ }
+
+ @Override
+ public void close() {
+ try {
+ // Safe to call multiple times. Both Os.close(FileDescriptor) and
+ // ParcelFileDescriptor#close() offer protection against double-closing an fd.
+ mFd.close();
+ } catch (IOException e) {
+ Log.w(TAG, "close: Failed to close fd", e);
+ }
+ }
+ }
+
+ private class L2capThread extends Thread {
+ // Set mBuffer length to MTU + 1 to catch read() overflows.
+ private final byte[] mBuffer = new byte[MTU + 1];
+ private volatile boolean mIsRunning = true;
+
+ private final String mLogTag;
+ private IReadWriteFd mReadFd;
+ private IReadWriteFd mWriteFd;
+
+ L2capThread(String logTag, IReadWriteFd readFd, IReadWriteFd writeFd) {
+ mLogTag = logTag;
+ mReadFd = readFd;
+ mWriteFd = writeFd;
+ }
+
+ private void postOnError() {
+ mHandler.post(() -> {
+ // All callbacks must be called on handler thread.
+ mCallback.onError();
+ });
+ }
+
+ @Override
+ public void run() {
+ while (mIsRunning) {
+ try {
+ final int readBytes = mReadFd.read(mBuffer, 0 /*off*/, mBuffer.length);
+ // No bytes to write, continue.
+ if (readBytes <= 0) {
+ Log.w(mLogTag, "Zero-byte read encountered: " + readBytes);
+ continue;
+ }
+
+ // If the packet exceeds MTU, drop it.
+ // Note that a large read on BluetoothSocket throws an IOException to tear down
+ // the network.
+ if (readBytes > MTU) continue;
+
+ mWriteFd.write(mBuffer, 0 /*off*/, readBytes);
+ } catch (IOException e) {
+ Log.e(mLogTag, "L2capThread exception", e);
+ // Tear down the network on any error.
+ mIsRunning = false;
+ // Notify provider that forwarding has stopped.
+ postOnError();
+ }
+ }
+ }
+
+ public void tearDown() {
+ mIsRunning = false;
+ mReadFd.shutdownRead();
+ mWriteFd.shutdownWrite();
+ }
+ }
+
+ public L2capPacketForwarder(Handler handler, ParcelFileDescriptor tunFd, BluetoothSocket socket,
+ ICallback cb) {
+ this(handler, new FdWrapper(tunFd), new BluetoothSocketWrapper(socket), cb);
+ }
+
+ @VisibleForTesting
+ L2capPacketForwarder(Handler handler, IReadWriteFd tunFd, IReadWriteFd l2capFd, ICallback cb) {
+ mHandler = handler;
+ mTunFd = tunFd;
+ mL2capFd = l2capFd;
+ mCallback = cb;
+
+ mIngressThread = new L2capThread("L2capThread-Ingress", l2capFd, tunFd);
+ mEgressThread = new L2capThread("L2capThread-Egress", tunFd, l2capFd);
+
+ mIngressThread.start();
+ mEgressThread.start();
+ }
+
+ /**
+ * Tear down the L2capPacketForwarder.
+ *
+ * This operation closes both the passed tun fd and BluetoothSocket.
+ **/
+ public void tearDown() {
+ // Destroying both threads first guarantees that both read and write side of FD have been
+ // shutdown.
+ mIngressThread.tearDown();
+ mEgressThread.tearDown();
+
+ // In order to interrupt a blocking read on the BluetoothSocket, the BluetoothSocket must be
+ // closed (which triggers shutdown()). This means, the BluetoothSocket must be closed inside
+ // L2capPacketForwarder. Tear down the tun fd alongside it for consistency.
+ mTunFd.close();
+ mL2capFd.close();
+
+ try {
+ mIngressThread.join();
+ } catch (InterruptedException e) {
+ // join() interrupted in tearDown path, do nothing.
+ }
+ try {
+ mEgressThread.join();
+ } catch (InterruptedException e) {
+ // join() interrupted in tearDown path, do nothing.
+ }
+ }
+}
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index b4a3b8a..0eab6e7 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -350,7 +350,7 @@
// TODO: remove "apex_available:platform".
apex_available: [
"//apex_available:platform",
- "com.android.btservices",
+ "com.android.bt",
"com.android.tethering",
"com.android.wifi",
],
diff --git a/staticlibs/device/com/android/net/module/util/RealtimeScheduler.java b/staticlibs/device/com/android/net/module/util/RealtimeScheduler.java
new file mode 100644
index 0000000..c8fdf72
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/RealtimeScheduler.java
@@ -0,0 +1,345 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR;
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.MessageQueue;
+import android.os.ParcelFileDescriptor;
+import android.os.SystemClock;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.CloseGuard;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import java.io.IOException;
+import java.util.PriorityQueue;
+
+/**
+ * Represents a realtime scheduler object used for scheduling tasks with precise delays.
+ * Compared to {@link Handler#postDelayed}, this class offers enhanced accuracy for delayed
+ * callbacks by accounting for periods when the device is in deep sleep.
+ *
+ * <p> This class is designed for use exclusively from the handler thread.
+ *
+ * **Usage Examples:**
+ *
+ * ** Scheduling recurring tasks with the same RealtimeScheduler **
+ *
+ * ```java
+ * // Create a RealtimeScheduler
+ * final RealtimeScheduler scheduler = new RealtimeScheduler(handler);
+ *
+ * // Schedule a new task with a delay.
+ * scheduler.postDelayed(() -> taskToExecute(), delayTime);
+ *
+ * // Once the delay has elapsed, and the task is running, schedule another task.
+ * scheduler.postDelayed(() -> anotherTaskToExecute(), anotherDelayTime);
+ *
+ * // Remember to close the RealtimeScheduler after all tasks have finished running.
+ * scheduler.close();
+ * ```
+ */
+public class RealtimeScheduler {
+ private static final String TAG = RealtimeScheduler.class.getSimpleName();
+ // EVENT_ERROR may be generated even if not specified, as per its javadoc.
+ private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR;
+ private final CloseGuard mGuard = new CloseGuard();
+ @NonNull
+ private final Handler mHandler;
+ @NonNull
+ private final MessageQueue mQueue;
+ @NonNull
+ private final ParcelFileDescriptor mParcelFileDescriptor;
+ private final int mFdInt;
+
+ private final PriorityQueue<Task> mTaskQueue;
+
+ /**
+ * An abstract class for defining tasks that can be executed using a {@link Handler}.
+ */
+ private abstract static class Task implements Comparable<Task> {
+ private final long mRunTimeMs;
+ private final long mCreatedTimeNs = SystemClock.elapsedRealtimeNanos();
+
+ /**
+ * create a task with a run time
+ */
+ Task(long runTimeMs) {
+ mRunTimeMs = runTimeMs;
+ }
+
+ /**
+ * Executes the task using the provided {@link Handler}.
+ *
+ * @param handler The {@link Handler} to use for executing the task.
+ */
+ abstract void post(Handler handler);
+
+ @Override
+ public int compareTo(@NonNull Task o) {
+ if (mRunTimeMs != o.mRunTimeMs) {
+ return Long.compare(mRunTimeMs, o.mRunTimeMs);
+ }
+ return Long.compare(mCreatedTimeNs, o.mCreatedTimeNs);
+ }
+
+ /**
+ * Returns the run time of the task.
+ */
+ public long getRunTimeMs() {
+ return mRunTimeMs;
+ }
+ }
+
+ /**
+ * A task that sends a {@link Message} using a {@link Handler}.
+ */
+ private static class MessageTask extends Task {
+ private final Message mMessage;
+
+ MessageTask(Message message, long runTimeMs) {
+ super(runTimeMs);
+ mMessage = message;
+ }
+
+ /**
+ * Sends the {@link Message} using the provided {@link Handler}.
+ *
+ * @param handler The {@link Handler} to use for sending the message.
+ */
+ @Override
+ public void post(Handler handler) {
+ handler.sendMessage(mMessage);
+ }
+ }
+
+ /**
+ * A task that posts a {@link Runnable} to a {@link Handler}.
+ */
+ private static class RunnableTask extends Task {
+ private final Runnable mRunnable;
+
+ RunnableTask(Runnable runnable, long runTimeMs) {
+ super(runTimeMs);
+ mRunnable = runnable;
+ }
+
+ /**
+ * Posts the {@link Runnable} to the provided {@link Handler}.
+ *
+ * @param handler The {@link Handler} to use for posting the runnable.
+ */
+ @Override
+ public void post(Handler handler) {
+ handler.post(mRunnable);
+ }
+ }
+
+ /**
+ * The RealtimeScheduler constructor
+ *
+ * Note: The constructor is currently safe to call on another thread because it only sets final
+ * members and registers the event to be called on the handler.
+ */
+ public RealtimeScheduler(@NonNull Handler handler) {
+ mFdInt = TimerFdUtils.createTimerFileDescriptor();
+ mParcelFileDescriptor = ParcelFileDescriptor.adoptFd(mFdInt);
+ mHandler = handler;
+ mQueue = handler.getLooper().getQueue();
+ mTaskQueue = new PriorityQueue<>();
+ registerFdEventListener();
+
+ mGuard.open("close");
+ }
+
+ private boolean enqueueTask(@NonNull Task task, long delayMs) {
+ ensureRunningOnCorrectThread();
+ if (delayMs <= 0L) {
+ task.post(mHandler);
+ return true;
+ }
+ if (mTaskQueue.isEmpty() || task.compareTo(mTaskQueue.peek()) < 0) {
+ if (!TimerFdUtils.setExpirationTime(mFdInt, delayMs)) {
+ return false;
+ }
+ }
+ mTaskQueue.add(task);
+ return true;
+ }
+
+ /**
+ * Set a runnable to be executed after a specified delay.
+ *
+ * If delayMs is less than or equal to 0, the runnable will be executed immediately.
+ *
+ * @param runnable the runnable to be executed
+ * @param delayMs the delay time in milliseconds
+ * @return true if the task is scheduled successfully, false otherwise.
+ */
+ public boolean postDelayed(@NonNull Runnable runnable, long delayMs) {
+ return enqueueTask(new RunnableTask(runnable, SystemClock.elapsedRealtime() + delayMs),
+ delayMs);
+ }
+
+ /**
+ * Remove a scheduled runnable.
+ *
+ * @param runnable the runnable to be removed
+ */
+ public void removeDelayedRunnable(@NonNull Runnable runnable) {
+ ensureRunningOnCorrectThread();
+ mTaskQueue.removeIf(task -> task instanceof RunnableTask
+ && ((RunnableTask) task).mRunnable == runnable);
+ }
+
+ /**
+ * Set a message to be sent after a specified delay.
+ *
+ * If delayMs is less than or equal to 0, the message will be sent immediately.
+ *
+ * @param msg the message to be sent
+ * @param delayMs the delay time in milliseconds
+ * @return true if the message is scheduled successfully, false otherwise.
+ */
+ public boolean sendDelayedMessage(Message msg, long delayMs) {
+
+ return enqueueTask(new MessageTask(msg, SystemClock.elapsedRealtime() + delayMs), delayMs);
+ }
+
+ /**
+ * Remove a scheduled message.
+ *
+ * @param what the message to be removed
+ */
+ public void removeDelayedMessage(int what) {
+ ensureRunningOnCorrectThread();
+ mTaskQueue.removeIf(task -> task instanceof MessageTask
+ && ((MessageTask) task).mMessage.what == what);
+ }
+
+ /**
+ * Close the RealtimeScheduler. This implementation closes the underlying
+ * OS resources allocated to represent this stream.
+ */
+ public void close() {
+ ensureRunningOnCorrectThread();
+ unregisterAndDestroyFd();
+ }
+
+ private void registerFdEventListener() {
+ mQueue.addOnFileDescriptorEventListener(
+ mParcelFileDescriptor.getFileDescriptor(),
+ FD_EVENTS,
+ (fd, events) -> {
+ if (!isRunning()) {
+ return 0;
+ }
+ if ((events & EVENT_ERROR) != 0) {
+ Log.wtf(TAG, "Got EVENT_ERROR from FileDescriptorEventListener.");
+ return 0;
+ }
+ if ((events & EVENT_INPUT) != 0) {
+ handleExpiration();
+ }
+ return FD_EVENTS;
+ });
+ }
+
+ private boolean isRunning() {
+ return mParcelFileDescriptor.getFileDescriptor().valid();
+ }
+
+ private void handleExpiration() {
+ // The data from the FileDescriptor must be read after the timer expires. Otherwise,
+ // expiration callbacks will continue to be sent, notifying of unread data. The content(the
+ // number of expirations) can be ignored, as the callback is the only item of interest.
+ // Refer to https://man7.org/linux/man-pages/man2/timerfd_create.2.html
+ // read(2)
+ // If the timer has already expired one or more times since
+ // its settings were last modified using timerfd_settime(),
+ // or since the last successful read(2), then the buffer
+ // given to read(2) returns an unsigned 8-byte integer
+ // (uint64_t) containing the number of expirations that have
+ // occurred. (The returned value is in host byte order—that
+ // is, the native byte order for integers on the host
+ // machine.)
+ final byte[] readBuffer = new byte[8];
+ try {
+ Os.read(mParcelFileDescriptor.getFileDescriptor(), readBuffer, 0, readBuffer.length);
+ } catch (IOException | ErrnoException exception) {
+ Log.wtf(TAG, "Read FileDescriptor failed. ", exception);
+ }
+
+ long currentTimeMs = SystemClock.elapsedRealtime();
+ while (!mTaskQueue.isEmpty()) {
+ final Task task = mTaskQueue.peek();
+ currentTimeMs = SystemClock.elapsedRealtime();
+ if (currentTimeMs < task.getRunTimeMs()) {
+ break;
+ }
+ task.post(mHandler);
+ mTaskQueue.poll();
+ }
+
+ if (!mTaskQueue.isEmpty()) {
+ // Using currentTimeMs ensures that the calculated expiration time
+ // is always positive.
+ if (!TimerFdUtils.setExpirationTime(mFdInt,
+ mTaskQueue.peek().getRunTimeMs() - currentTimeMs)) {
+ // If setting the expiration time fails, clear the task queue.
+ Log.wtf(TAG, "Failed to set expiration time");
+ mTaskQueue.clear();
+ }
+ }
+ }
+
+ private void unregisterAndDestroyFd() {
+ if (mGuard != null) {
+ mGuard.close();
+ }
+
+ mQueue.removeOnFileDescriptorEventListener(mParcelFileDescriptor.getFileDescriptor());
+ try {
+ mParcelFileDescriptor.close();
+ } catch (IOException exception) {
+ Log.e(TAG, "close ParcelFileDescriptor failed. ", exception);
+ }
+ }
+
+ private void ensureRunningOnCorrectThread() {
+ if (mHandler.getLooper() != Looper.myLooper()) {
+ throw new IllegalStateException(
+ "Not running on Handler thread: " + Thread.currentThread().getName());
+ }
+ }
+
+ @SuppressWarnings("Finalize")
+ @Override
+ protected void finalize() throws Throwable {
+ if (mGuard != null) {
+ mGuard.warnIfOpen();
+ }
+ super.finalize();
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/ServiceConnectivityJni.java b/staticlibs/device/com/android/net/module/util/ServiceConnectivityJni.java
new file mode 100644
index 0000000..4a5dd4f
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/ServiceConnectivityJni.java
@@ -0,0 +1,63 @@
+/*
+ * 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.net.module.util;
+
+import android.annotation.NonNull;
+
+import java.io.IOException;
+
+/**
+ * Contains JNI functions for use in service-connectivity
+ */
+public class ServiceConnectivityJni {
+ static {
+ final String libName = JniUtil.getJniLibraryName(ServiceConnectivityJni.class.getPackage());
+ if (libName.equals("android_net_connectivity_com_android_net_module_util_jni")) {
+ // This library is part of service-connectivity.jar when in the system server,
+ // so libservice-connectivity.so is the library to load.
+ System.loadLibrary("service-connectivity");
+ } else {
+ System.loadLibrary(libName);
+ }
+ }
+
+ /**
+ * Create a timerfd.
+ *
+ * @throws IOException if the timerfd creation is failed.
+ */
+ public static native int createTimerFd() throws IOException;
+
+ /**
+ * Set given time to the timerfd.
+ *
+ * @param timeMs target time
+ * @throws IOException if setting expiration time is failed.
+ */
+ public static native void setTimerFdTime(int fd, long timeMs) throws IOException;
+
+ /** Create tun/tap interface */
+ public static native int createTunTap(boolean isTun, boolean hasCarrier,
+ boolean setIffMulticast, @NonNull String iface);
+
+ /** Enable carrier on tun/tap interface */
+ public static native void setTunTapCarrierEnabled(@NonNull String iface, int tunFd,
+ boolean enabled);
+
+ /** Bring up tun/tap interface */
+ public static native void bringUpInterface(String iface);
+}
diff --git a/staticlibs/device/com/android/net/module/util/TimerFdUtils.java b/staticlibs/device/com/android/net/module/util/TimerFdUtils.java
index f0de142..10bc595 100644
--- a/staticlibs/device/com/android/net/module/util/TimerFdUtils.java
+++ b/staticlibs/device/com/android/net/module/util/TimerFdUtils.java
@@ -25,40 +25,14 @@
* Contains mostly timerfd functionality.
*/
public class TimerFdUtils {
- static {
- final String jniLibName = JniUtil.getJniLibraryName(TimerFdUtils.class.getPackage());
- if (jniLibName.equals("android_net_connectivity_com_android_net_module_util_jni")) {
- // This library is part of service-connectivity.jar when in the system server,
- // so libservice-connectivity.so is the library to load.
- System.loadLibrary("service-connectivity");
- } else {
- System.loadLibrary(jniLibName);
- }
- }
-
private static final String TAG = TimerFdUtils.class.getSimpleName();
/**
- * Create a timerfd.
- *
- * @throws IOException if the timerfd creation is failed.
- */
- private static native int createTimerFd() throws IOException;
-
- /**
- * Set given time to the timerfd.
- *
- * @param timeMs target time
- * @throws IOException if setting expiration time is failed.
- */
- private static native void setTime(int fd, long timeMs) throws IOException;
-
- /**
* Create a timerfd
*/
static int createTimerFileDescriptor() {
try {
- return createTimerFd();
+ return ServiceConnectivityJni.createTimerFd();
} catch (IOException e) {
Log.e(TAG, "createTimerFd failed", e);
return -1;
@@ -70,7 +44,7 @@
*/
static boolean setExpirationTime(int fd, long expirationTimeMs) {
try {
- setTime(fd, expirationTimeMs);
+ ServiceConnectivityJni.setTimerFdTime(fd, expirationTimeMs);
} catch (IOException e) {
Log.e(TAG, "setExpirationTime failed", e);
return false;
diff --git a/staticlibs/device/com/android/net/module/util/TimerFileDescriptor.java b/staticlibs/device/com/android/net/module/util/TimerFileDescriptor.java
deleted file mode 100644
index dbbccc5..0000000
--- a/staticlibs/device/com/android/net/module/util/TimerFileDescriptor.java
+++ /dev/null
@@ -1,254 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.net.module.util;
-
-import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR;
-import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
-
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-import android.os.MessageQueue;
-import android.os.ParcelFileDescriptor;
-import android.util.CloseGuard;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.io.IOException;
-
-/**
- * Represents a Timer file descriptor object used for scheduling tasks with precise delays.
- * Compared to {@link Handler#postDelayed}, this class offers enhanced accuracy for delayed
- * callbacks by accounting for periods when the device is in deep sleep.
- *
- * <p> This class is designed for use exclusively from the handler thread.
- *
- * **Usage Examples:**
- *
- * ** Scheduling recurring tasks with the same TimerFileDescriptor **
- *
- * ```java
- * // Create a TimerFileDescriptor
- * final TimerFileDescriptor timerFd = new TimerFileDescriptor(handler);
- *
- * // Schedule a new task with a delay.
- * timerFd.setDelayedTask(() -> taskToExecute(), delayTime);
- *
- * // Once the delay has elapsed, and the task is running, schedule another task.
- * timerFd.setDelayedTask(() -> anotherTaskToExecute(), anotherDelayTime);
- *
- * // Remember to close the TimerFileDescriptor after all tasks have finished running.
- * timerFd.close();
- * ```
- */
-public class TimerFileDescriptor {
- private static final String TAG = TimerFileDescriptor.class.getSimpleName();
- // EVENT_ERROR may be generated even if not specified, as per its javadoc.
- private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR;
- private final CloseGuard mGuard = new CloseGuard();
- @NonNull
- private final Handler mHandler;
- @NonNull
- private final MessageQueue mQueue;
- @NonNull
- private final ParcelFileDescriptor mParcelFileDescriptor;
- private final int mFdInt;
- @Nullable
- private ITask mTask;
-
- /**
- * An interface for defining tasks that can be executed using a {@link Handler}.
- */
- public interface ITask {
- /**
- * Executes the task using the provided {@link Handler}.
- *
- * @param handler The {@link Handler} to use for executing the task.
- */
- void post(Handler handler);
- }
-
- /**
- * A task that sends a {@link Message} using a {@link Handler}.
- */
- public static class MessageTask implements ITask {
- private final Message mMessage;
-
- public MessageTask(Message message) {
- mMessage = message;
- }
-
- /**
- * Sends the {@link Message} using the provided {@link Handler}.
- *
- * @param handler The {@link Handler} to use for sending the message.
- */
- @Override
- public void post(Handler handler) {
- handler.sendMessage(mMessage);
- }
- }
-
- /**
- * A task that posts a {@link Runnable} to a {@link Handler}.
- */
- public static class RunnableTask implements ITask {
- private final Runnable mRunnable;
-
- public RunnableTask(Runnable runnable) {
- mRunnable = runnable;
- }
-
- /**
- * Posts the {@link Runnable} to the provided {@link Handler}.
- *
- * @param handler The {@link Handler} to use for posting the runnable.
- */
- @Override
- public void post(Handler handler) {
- handler.post(mRunnable);
- }
- }
-
- /**
- * TimerFileDescriptor constructor
- *
- * Note: The constructor is currently safe to call on another thread because it only sets final
- * members and registers the event to be called on the handler.
- */
- public TimerFileDescriptor(@NonNull Handler handler) {
- mFdInt = TimerFdUtils.createTimerFileDescriptor();
- mParcelFileDescriptor = ParcelFileDescriptor.adoptFd(mFdInt);
- mHandler = handler;
- mQueue = handler.getLooper().getQueue();
- registerFdEventListener();
-
- mGuard.open("close");
- }
-
- /**
- * Set a task to be executed after a specified delay.
- *
- * <p> A task can only be scheduled once at a time. Cancel previous scheduled task before the
- * new task is scheduled.
- *
- * @param task the task to be executed
- * @param delayMs the delay time in milliseconds
- * @throws IllegalArgumentException if try to replace the current scheduled task
- * @throws IllegalArgumentException if the delay time is less than 0
- */
- public void setDelayedTask(@NonNull ITask task, long delayMs) {
- ensureRunningOnCorrectThread();
- if (mTask != null) {
- throw new IllegalArgumentException("task is already scheduled");
- }
- if (delayMs <= 0L) {
- task.post(mHandler);
- return;
- }
-
- if (TimerFdUtils.setExpirationTime(mFdInt, delayMs)) {
- mTask = task;
- }
- }
-
- /**
- * Cancel the scheduled task.
- */
- public void cancelTask() {
- ensureRunningOnCorrectThread();
- if (mTask == null) return;
-
- TimerFdUtils.setExpirationTime(mFdInt, 0 /* delayMs */);
- mTask = null;
- }
-
- /**
- * Check if there is a scheduled task.
- */
- public boolean hasDelayedTask() {
- ensureRunningOnCorrectThread();
- return mTask != null;
- }
-
- /**
- * Close the TimerFileDescriptor. This implementation closes the underlying
- * OS resources allocated to represent this stream.
- */
- public void close() {
- ensureRunningOnCorrectThread();
- unregisterAndDestroyFd();
- }
-
- private void registerFdEventListener() {
- mQueue.addOnFileDescriptorEventListener(
- mParcelFileDescriptor.getFileDescriptor(),
- FD_EVENTS,
- (fd, events) -> {
- if (!isRunning()) {
- return 0;
- }
- if ((events & EVENT_INPUT) != 0) {
- handleExpiration();
- }
- return FD_EVENTS;
- });
- }
-
- private boolean isRunning() {
- return mParcelFileDescriptor.getFileDescriptor().valid();
- }
-
- private void handleExpiration() {
- // Execute the task
- if (mTask != null) {
- mTask.post(mHandler);
- mTask = null;
- }
- }
-
- private void unregisterAndDestroyFd() {
- if (mGuard != null) {
- mGuard.close();
- }
-
- mQueue.removeOnFileDescriptorEventListener(mParcelFileDescriptor.getFileDescriptor());
- try {
- mParcelFileDescriptor.close();
- } catch (IOException exception) {
- Log.e(TAG, "close ParcelFileDescriptor failed. ", exception);
- }
- }
-
- private void ensureRunningOnCorrectThread() {
- if (mHandler.getLooper() != Looper.myLooper()) {
- throw new IllegalStateException(
- "Not running on Handler thread: " + Thread.currentThread().getName());
- }
- }
-
- @SuppressWarnings("Finalize")
- @Override
- protected void finalize() throws Throwable {
- if (mGuard != null) {
- mGuard.warnIfOpen();
- }
- super.finalize();
- }
-}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
index e2544d3..0d96fc4 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
@@ -83,7 +83,7 @@
public static final int INET_DIAG_INFO = 2;
public static final int INET_DIAG_MARK = 15;
- public static final long IO_TIMEOUT_MS = 300L;
+ public static final long IO_TIMEOUT_MS = 3000L;
public static final int DEFAULT_RECV_BUFSIZE = 8 * 1024;
public static final int SOCKET_RECV_BUFSIZE = 64 * 1024;
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
index 1afe3b8..f17a7ec 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
@@ -357,7 +357,8 @@
// interface, including change in administrative state. While RTM_SETLINK is used to
// modify an existing link rather than creating a new one.
return RtNetlinkLinkMessage.build(
- new StructNlMsgHdr(/*payloadLen*/ 0, RTM_NEWLINK, NLM_F_REQUEST, sequenceNumber),
+ new StructNlMsgHdr(
+ /*payloadLen*/ 0, RTM_NEWLINK, NLM_F_REQUEST_ACK, sequenceNumber),
new StructIfinfoMsg((short) AF_UNSPEC, /*type*/ 0, interfaceIndex,
flagsBits, changeBits),
DEFAULT_MTU, /*hardwareAddress*/ null, /*interfaceName*/ null);
diff --git a/staticlibs/framework/com/android/net/module/util/TerribleErrorLog.java b/staticlibs/framework/com/android/net/module/util/TerribleErrorLog.java
new file mode 100644
index 0000000..b4f7642
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/TerribleErrorLog.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import android.annotation.NonNull;
+import android.util.Log;
+
+import java.util.function.BiConsumer;
+
+/**
+ * Utility class for logging terrible errors and reporting them for tracking.
+ *
+ * @hide
+ */
+public class TerribleErrorLog {
+
+ private static final String TAG = TerribleErrorLog.class.getSimpleName();
+
+ /**
+ * Logs a terrible error and reports metrics through a provided statsLog.
+ */
+ public static void logTerribleError(@NonNull BiConsumer<Integer, Integer> statsLog,
+ @NonNull String message, int protoType, int errorType) {
+ statsLog.accept(protoType, errorType);
+ Log.wtf(TAG, message);
+ }
+}
diff --git a/staticlibs/native/timerfdutils/Android.bp b/staticlibs/native/serviceconnectivityjni/Android.bp
similarity index 86%
rename from staticlibs/native/timerfdutils/Android.bp
rename to staticlibs/native/serviceconnectivityjni/Android.bp
index 939a2d2..18246dd 100644
--- a/staticlibs/native/timerfdutils/Android.bp
+++ b/staticlibs/native/serviceconnectivityjni/Android.bp
@@ -18,17 +18,20 @@
}
cc_library_static {
- name: "libnet_utils_device_common_timerfdjni",
+ name: "libserviceconnectivityjni",
srcs: [
- "com_android_net_module_util_TimerFdUtils.cpp",
+ "com_android_net_module_util_ServiceConnectivityJni.cpp",
],
header_libs: [
+ "bpf_headers",
"jni_headers",
+ "libbase_headers",
],
shared_libs: [
"liblog",
"libnativehelper_compat_libc++",
],
+ stl: "libc++_static",
cflags: [
"-Wall",
"-Werror",
diff --git a/staticlibs/native/serviceconnectivityjni/com_android_net_module_util_ServiceConnectivityJni.cpp b/staticlibs/native/serviceconnectivityjni/com_android_net_module_util_ServiceConnectivityJni.cpp
new file mode 100644
index 0000000..8767589
--- /dev/null
+++ b/staticlibs/native/serviceconnectivityjni/com_android_net_module_util_ServiceConnectivityJni.cpp
@@ -0,0 +1,209 @@
+/*
+ * 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.
+ */
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <jni.h>
+#include <linux/if.h>
+#include <linux/if_tun.h>
+#include <linux/ipv6_route.h>
+#include <linux/route.h>
+#include <netinet/in.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <string>
+#include <sys/epoll.h>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/timerfd.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <android-base/unique_fd.h>
+#include <bpf/KernelUtils.h>
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/scoped_utf_chars.h>
+
+#define MSEC_PER_SEC 1000
+#define NSEC_PER_MSEC 1000000
+
+#ifndef IFF_NO_CARRIER
+#define IFF_NO_CARRIER 0x0040
+#endif
+
+namespace android {
+
+static jint createTimerFd(JNIEnv *env, jclass clazz) {
+ int tfd;
+ // For safety, the file descriptor should have O_NONBLOCK(TFD_NONBLOCK) set
+ // using fcntl during creation. This ensures that, in the worst-case scenario,
+ // an EAGAIN error is returned when reading.
+ tfd = timerfd_create(CLOCK_BOOTTIME, TFD_NONBLOCK);
+ if (tfd == -1) {
+ jniThrowErrnoException(env, "createTimerFd", tfd);
+ }
+ return tfd;
+}
+
+static void setTimerFdTime(JNIEnv *env, jclass clazz, jint tfd,
+ jlong milliseconds) {
+ struct itimerspec new_value;
+ new_value.it_value.tv_sec = milliseconds / MSEC_PER_SEC;
+ new_value.it_value.tv_nsec = (milliseconds % MSEC_PER_SEC) * NSEC_PER_MSEC;
+ // Set the interval time to 0 because it's designed for repeated timer
+ // expirations after the initial expiration, which doesn't fit the current
+ // usage.
+ new_value.it_interval.tv_sec = 0;
+ new_value.it_interval.tv_nsec = 0;
+
+ int ret = timerfd_settime(tfd, 0, &new_value, NULL);
+ if (ret == -1) {
+ jniThrowErrnoException(env, "setTimerFdTime", ret);
+ }
+}
+
+static void throwException(JNIEnv *env, int error, const char *action,
+ const char *iface) {
+ const std::string &msg = "Error: " + std::string(action) + " " +
+ std::string(iface) + ": " +
+ std::string(strerror(error));
+ jniThrowException(env, "java/lang/IllegalStateException", msg.c_str());
+}
+
+// enable or disable carrier on tun / tap interface.
+static void setTunTapCarrierEnabledImpl(JNIEnv *env, const char *iface,
+ int tunFd, bool enabled) {
+ uint32_t carrierOn = enabled;
+ if (ioctl(tunFd, TUNSETCARRIER, &carrierOn)) {
+ throwException(env, errno, "set carrier", iface);
+ }
+}
+
+static int createTunTapImpl(JNIEnv *env, bool isTun, bool hasCarrier,
+ bool setIffMulticast, const char *iface) {
+ base::unique_fd tun(open("/dev/tun", O_RDWR | O_NONBLOCK));
+ ifreq ifr{};
+
+ // Allocate interface.
+ ifr.ifr_flags = (isTun ? IFF_TUN : IFF_TAP) | IFF_NO_PI;
+ if (!hasCarrier) {
+ // Using IFF_NO_CARRIER is supported starting in kernel version >= 6.0
+ // Up until then, unsupported flags are ignored.
+ if (!bpf::isAtLeastKernelVersion(6, 0, 0)) {
+ throwException(env, EOPNOTSUPP, "IFF_NO_CARRIER not supported",
+ ifr.ifr_name);
+ return -1;
+ }
+ ifr.ifr_flags |= IFF_NO_CARRIER;
+ }
+ strlcpy(ifr.ifr_name, iface, IFNAMSIZ);
+ if (ioctl(tun.get(), TUNSETIFF, &ifr)) {
+ throwException(env, errno, "allocating", ifr.ifr_name);
+ return -1;
+ }
+
+ // Mark some TAP interfaces as supporting multicast
+ if (setIffMulticast && !isTun) {
+ base::unique_fd inet6CtrlSock(socket(AF_INET6, SOCK_DGRAM, 0));
+ ifr.ifr_flags = IFF_MULTICAST;
+
+ if (ioctl(inet6CtrlSock.get(), SIOCSIFFLAGS, &ifr)) {
+ throwException(env, errno, "set IFF_MULTICAST", ifr.ifr_name);
+ return -1;
+ }
+ }
+
+ return tun.release();
+}
+
+static void bringUpInterfaceImpl(JNIEnv *env, const char *iface) {
+ // Activate interface using an unconnected datagram socket.
+ base::unique_fd inet6CtrlSock(socket(AF_INET6, SOCK_DGRAM, 0));
+
+ ifreq ifr{};
+ strlcpy(ifr.ifr_name, iface, IFNAMSIZ);
+ if (ioctl(inet6CtrlSock.get(), SIOCGIFFLAGS, &ifr)) {
+ throwException(env, errno, "read flags", iface);
+ return;
+ }
+ ifr.ifr_flags |= IFF_UP;
+ if (ioctl(inet6CtrlSock.get(), SIOCSIFFLAGS, &ifr)) {
+ throwException(env, errno, "set IFF_UP", iface);
+ return;
+ }
+}
+
+//------------------------------------------------------------------------------
+
+static void setTunTapCarrierEnabled(JNIEnv *env, jclass /* clazz */,
+ jstring jIface, jint tunFd,
+ jboolean enabled) {
+ ScopedUtfChars iface(env, jIface);
+ if (!iface.c_str()) {
+ jniThrowNullPointerException(env, "iface");
+ return;
+ }
+ setTunTapCarrierEnabledImpl(env, iface.c_str(), tunFd, enabled);
+}
+
+static jint createTunTap(JNIEnv *env, jclass /* clazz */, jboolean isTun,
+ jboolean hasCarrier, jboolean setIffMulticast,
+ jstring jIface) {
+ ScopedUtfChars iface(env, jIface);
+ if (!iface.c_str()) {
+ jniThrowNullPointerException(env, "iface");
+ return -1;
+ }
+
+ return createTunTapImpl(env, isTun, hasCarrier, setIffMulticast,
+ iface.c_str());
+}
+
+static void bringUpInterface(JNIEnv *env, jclass /* clazz */, jstring jIface) {
+ ScopedUtfChars iface(env, jIface);
+ if (!iface.c_str()) {
+ jniThrowNullPointerException(env, "iface");
+ return;
+ }
+ bringUpInterfaceImpl(env, iface.c_str());
+}
+
+//------------------------------------------------------------------------------
+
+/*
+ * JNI registration.
+ */
+static const JNINativeMethod gMethods[] = {
+ /* name, signature, funcPtr */
+ {"createTimerFd", "()I", (void *)createTimerFd},
+ {"setTimerFdTime", "(IJ)V", (void *)setTimerFdTime},
+ {"setTunTapCarrierEnabled", "(Ljava/lang/String;IZ)V",
+ (void *)setTunTapCarrierEnabled},
+ {"createTunTap", "(ZZZLjava/lang/String;)I", (void *)createTunTap},
+ {"bringUpInterface", "(Ljava/lang/String;)V", (void *)bringUpInterface},
+};
+
+int register_com_android_net_module_util_ServiceConnectivityJni(
+ JNIEnv *env, char const *class_name) {
+ return jniRegisterNativeMethods(env, class_name, gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/staticlibs/native/tcutils/tcutils.cpp b/staticlibs/native/tcutils/tcutils.cpp
index 21e781c..5425d0e 100644
--- a/staticlibs/native/tcutils/tcutils.cpp
+++ b/staticlibs/native/tcutils/tcutils.cpp
@@ -361,7 +361,7 @@
const sockaddr_nl KERNEL_NLADDR = {AF_NETLINK, 0, 0, 0};
const uint16_t NETLINK_REQUEST_FLAGS = NLM_F_REQUEST | NLM_F_ACK;
-int sendAndProcessNetlinkResponse(const void *req, int len) {
+int sendAndProcessNetlinkResponse(const void *req, int len, bool enoent_ok) {
// TODO: use unique_fd instead of ScopeGuard
unique_fd fd(socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE));
if (!fd.ok()) {
@@ -445,7 +445,9 @@
return -ENOMSG;
}
- if (resp.e.error) {
+ if (resp.e.error == -ENOENT) {
+ if (!enoent_ok) ALOGE("NLMSG_ERROR message returned ENOENT");
+ } else if (resp.e.error) {
ALOGE("NLMSG_ERROR message return error: %d", resp.e.error);
}
return resp.e.error; // returns 0 on success
@@ -560,7 +562,8 @@
};
#undef CLSACT
- return sendAndProcessNetlinkResponse(&req, sizeof(req));
+ const bool enoent_ok = (nlMsgType == RTM_DELQDISC);
+ return sendAndProcessNetlinkResponse(&req, sizeof(req), enoent_ok);
}
// tc filter add dev .. in/egress prio 1 protocol ipv6/ip bpf object-pinned
@@ -666,7 +669,7 @@
snprintf(req.options.name.str, sizeof(req.options.name.str), "%s:[*fsobj]",
basename(bpfProgPath));
- int error = sendAndProcessNetlinkResponse(&req, sizeof(req));
+ int error = sendAndProcessNetlinkResponse(&req, sizeof(req), false);
return error;
}
@@ -698,7 +701,8 @@
return error;
}
return sendAndProcessNetlinkResponse(filter.getRequest(),
- filter.getRequestSize());
+ filter.getRequestSize(),
+ false);
}
// tc filter del dev .. in/egress prio .. protocol ..
@@ -726,7 +730,7 @@
},
};
- return sendAndProcessNetlinkResponse(&req, sizeof(req));
+ return sendAndProcessNetlinkResponse(&req, sizeof(req), true);
}
} // namespace android
diff --git a/staticlibs/native/timerfdutils/com_android_net_module_util_TimerFdUtils.cpp b/staticlibs/native/timerfdutils/com_android_net_module_util_TimerFdUtils.cpp
deleted file mode 100644
index c4c960d..0000000
--- a/staticlibs/native/timerfdutils/com_android_net_module_util_TimerFdUtils.cpp
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include <errno.h>
-#include <jni.h>
-#include <nativehelper/JNIHelp.h>
-#include <nativehelper/scoped_utf_chars.h>
-#include <stdint.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/epoll.h>
-#include <sys/timerfd.h>
-#include <time.h>
-#include <unistd.h>
-
-#define MSEC_PER_SEC 1000
-#define NSEC_PER_MSEC 1000000
-
-namespace android {
-
-static jint
-com_android_net_module_util_TimerFdUtils_createTimerFd(JNIEnv *env,
- jclass clazz) {
- int tfd;
- tfd = timerfd_create(CLOCK_BOOTTIME, 0);
- if (tfd == -1) {
- jniThrowErrnoException(env, "createTimerFd", tfd);
- }
- return tfd;
-}
-
-static void
-com_android_net_module_util_TimerFdUtils_setTime(JNIEnv *env, jclass clazz,
- jint tfd, jlong milliseconds) {
- struct itimerspec new_value;
- new_value.it_value.tv_sec = milliseconds / MSEC_PER_SEC;
- new_value.it_value.tv_nsec = (milliseconds % MSEC_PER_SEC) * NSEC_PER_MSEC;
- // Set the interval time to 0 because it's designed for repeated timer expirations after the
- // initial expiration, which doesn't fit the current usage.
- new_value.it_interval.tv_sec = 0;
- new_value.it_interval.tv_nsec = 0;
-
- int ret = timerfd_settime(tfd, 0, &new_value, NULL);
- if (ret == -1) {
- jniThrowErrnoException(env, "setTime", ret);
- }
-}
-
-/*
- * JNI registration.
- */
-static const JNINativeMethod gMethods[] = {
- /* name, signature, funcPtr */
- {"createTimerFd", "()I",
- (void *)com_android_net_module_util_TimerFdUtils_createTimerFd},
- {"setTime", "(IJ)V",
- (void *)com_android_net_module_util_TimerFdUtils_setTime},
-};
-
-int register_com_android_net_module_util_TimerFdUtils(JNIEnv *env,
- char const *class_name) {
- return jniRegisterNativeMethods(env, class_name, gMethods, NELEM(gMethods));
-}
-
-}; // namespace android
diff --git a/staticlibs/netd/Android.bp b/staticlibs/netd/Android.bp
index 44abba2..03f5f06 100644
--- a/staticlibs/netd/Android.bp
+++ b/staticlibs/netd/Android.bp
@@ -22,7 +22,7 @@
sdk_version: "system_current",
min_sdk_version: "30",
static_libs: [
- "netd_aidl_interface-V15-java",
+ "netd_aidl_interface-V16-java",
],
apex_available: [
"//apex_available:platform", // used from services.net
@@ -45,7 +45,7 @@
cc_library_static {
name: "netd_aidl_interface-lateststable-ndk",
whole_static_libs: [
- "netd_aidl_interface-V15-ndk",
+ "netd_aidl_interface-V16-ndk",
],
apex_available: [
"com.android.resolv",
@@ -56,12 +56,12 @@
cc_defaults {
name: "netd_aidl_interface_lateststable_cpp_static",
- static_libs: ["netd_aidl_interface-V15-cpp"],
+ static_libs: ["netd_aidl_interface-V16-cpp"],
}
cc_defaults {
name: "netd_aidl_interface_lateststable_cpp_shared",
- shared_libs: ["netd_aidl_interface-V15-cpp"],
+ shared_libs: ["netd_aidl_interface-V16-cpp"],
}
aidl_interface {
@@ -171,6 +171,10 @@
version: "15",
imports: [],
},
+ {
+ version: "16",
+ imports: [],
+ },
],
frozen: true,
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/.hash b/staticlibs/netd/aidl_api/netd_aidl_interface/16/.hash
new file mode 100644
index 0000000..08cd338
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/.hash
@@ -0,0 +1 @@
+28e20632b92e146787d32437a53aaa5ad39125b7
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/INetd.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/INetd.aidl
new file mode 100644
index 0000000..8351b56
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/INetd.aidl
@@ -0,0 +1,272 @@
+/**
+ * Copyright (c) 2016, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetd {
+ boolean isAlive();
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ boolean firewallReplaceUidChain(in @utf8InCpp String chainName, boolean isAllowlist, in int[] uids);
+ boolean bandwidthEnableDataSaver(boolean enable);
+ /**
+ * @deprecated use networkCreate() instead.
+ */
+ void networkCreatePhysical(int netId, int permission);
+ /**
+ * @deprecated use networkCreate() instead.
+ */
+ void networkCreateVpn(int netId, boolean secure);
+ void networkDestroy(int netId);
+ void networkAddInterface(int netId, in @utf8InCpp String iface);
+ void networkRemoveInterface(int netId, in @utf8InCpp String iface);
+ void networkAddUidRanges(int netId, in android.net.UidRangeParcel[] uidRanges);
+ void networkRemoveUidRanges(int netId, in android.net.UidRangeParcel[] uidRanges);
+ void networkRejectNonSecureVpn(boolean add, in android.net.UidRangeParcel[] uidRanges);
+ void socketDestroy(in android.net.UidRangeParcel[] uidRanges, in int[] exemptUids);
+ boolean tetherApplyDnsInterfaces();
+ android.net.TetherStatsParcel[] tetherGetStats();
+ void interfaceAddAddress(in @utf8InCpp String ifName, in @utf8InCpp String addrString, int prefixLength);
+ void interfaceDelAddress(in @utf8InCpp String ifName, in @utf8InCpp String addrString, int prefixLength);
+ @utf8InCpp String getProcSysNet(int ipversion, int which, in @utf8InCpp String ifname, in @utf8InCpp String parameter);
+ void setProcSysNet(int ipversion, int which, in @utf8InCpp String ifname, in @utf8InCpp String parameter, in @utf8InCpp String value);
+ void ipSecSetEncapSocketOwner(in ParcelFileDescriptor socket, int newUid);
+ int ipSecAllocateSpi(int transformId, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int spi);
+ void ipSecAddSecurityAssociation(int transformId, int mode, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int underlyingNetId, int spi, int markValue, int markMask, in @utf8InCpp String authAlgo, in byte[] authKey, in int authTruncBits, in @utf8InCpp String cryptAlgo, in byte[] cryptKey, in int cryptTruncBits, in @utf8InCpp String aeadAlgo, in byte[] aeadKey, in int aeadIcvBits, int encapType, int encapLocalPort, int encapRemotePort, int interfaceId);
+ void ipSecDeleteSecurityAssociation(int transformId, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int spi, int markValue, int markMask, int interfaceId);
+ void ipSecApplyTransportModeTransform(in ParcelFileDescriptor socket, int transformId, int direction, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int spi);
+ void ipSecRemoveTransportModeTransform(in ParcelFileDescriptor socket);
+ void ipSecAddSecurityPolicy(int transformId, int selAddrFamily, int direction, in @utf8InCpp String tmplSrcAddress, in @utf8InCpp String tmplDstAddress, int spi, int markValue, int markMask, int interfaceId);
+ void ipSecUpdateSecurityPolicy(int transformId, int selAddrFamily, int direction, in @utf8InCpp String tmplSrcAddress, in @utf8InCpp String tmplDstAddress, int spi, int markValue, int markMask, int interfaceId);
+ void ipSecDeleteSecurityPolicy(int transformId, int selAddrFamily, int direction, int markValue, int markMask, int interfaceId);
+ void ipSecAddTunnelInterface(in @utf8InCpp String deviceName, in @utf8InCpp String localAddress, in @utf8InCpp String remoteAddress, int iKey, int oKey, int interfaceId);
+ void ipSecUpdateTunnelInterface(in @utf8InCpp String deviceName, in @utf8InCpp String localAddress, in @utf8InCpp String remoteAddress, int iKey, int oKey, int interfaceId);
+ void ipSecRemoveTunnelInterface(in @utf8InCpp String deviceName);
+ void wakeupAddInterface(in @utf8InCpp String ifName, in @utf8InCpp String prefix, int mark, int mask);
+ void wakeupDelInterface(in @utf8InCpp String ifName, in @utf8InCpp String prefix, int mark, int mask);
+ void setIPv6AddrGenMode(in @utf8InCpp String ifName, int mode);
+ void idletimerAddInterface(in @utf8InCpp String ifName, int timeout, in @utf8InCpp String classLabel);
+ void idletimerRemoveInterface(in @utf8InCpp String ifName, int timeout, in @utf8InCpp String classLabel);
+ void strictUidCleartextPenalty(int uid, int policyPenalty);
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The clatd control plane moved to the mainline module starting in T. See ClatCoordinator.
+ */
+ @utf8InCpp String clatdStart(in @utf8InCpp String ifName, in @utf8InCpp String nat64Prefix);
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The clatd control plane moved to the mainline module starting in T. See ClatCoordinator.
+ */
+ void clatdStop(in @utf8InCpp String ifName);
+ boolean ipfwdEnabled();
+ @utf8InCpp String[] ipfwdGetRequesterList();
+ void ipfwdEnableForwarding(in @utf8InCpp String requester);
+ void ipfwdDisableForwarding(in @utf8InCpp String requester);
+ void ipfwdAddInterfaceForward(in @utf8InCpp String fromIface, in @utf8InCpp String toIface);
+ void ipfwdRemoveInterfaceForward(in @utf8InCpp String fromIface, in @utf8InCpp String toIface);
+ void bandwidthSetInterfaceQuota(in @utf8InCpp String ifName, long bytes);
+ void bandwidthRemoveInterfaceQuota(in @utf8InCpp String ifName);
+ void bandwidthSetInterfaceAlert(in @utf8InCpp String ifName, long bytes);
+ void bandwidthRemoveInterfaceAlert(in @utf8InCpp String ifName);
+ void bandwidthSetGlobalAlert(long bytes);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void bandwidthAddNaughtyApp(int uid);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void bandwidthRemoveNaughtyApp(int uid);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void bandwidthAddNiceApp(int uid);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void bandwidthRemoveNiceApp(int uid);
+ void tetherStart(in @utf8InCpp String[] dhcpRanges);
+ void tetherStop();
+ boolean tetherIsEnabled();
+ void tetherInterfaceAdd(in @utf8InCpp String ifName);
+ void tetherInterfaceRemove(in @utf8InCpp String ifName);
+ @utf8InCpp String[] tetherInterfaceList();
+ void tetherDnsSet(int netId, in @utf8InCpp String[] dnsAddrs);
+ @utf8InCpp String[] tetherDnsList();
+ void networkAddRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop);
+ void networkRemoveRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop);
+ void networkAddLegacyRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop, int uid);
+ void networkRemoveLegacyRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop, int uid);
+ int networkGetDefault();
+ void networkSetDefault(int netId);
+ void networkClearDefault();
+ void networkSetPermissionForNetwork(int netId, int permission);
+ void networkSetPermissionForUser(int permission, in int[] uids);
+ void networkClearPermissionForUser(in int[] uids);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void trafficSetNetPermForUids(int permission, in int[] uids);
+ void networkSetProtectAllow(int uid);
+ void networkSetProtectDeny(int uid);
+ boolean networkCanProtect(int uid);
+ void firewallSetFirewallType(int firewalltype);
+ void firewallSetInterfaceRule(in @utf8InCpp String ifName, int firewallRule);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void firewallSetUidRule(int childChain, int uid, int firewallRule);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void firewallEnableChildChain(int childChain, boolean enable);
+ @utf8InCpp String[] interfaceGetList();
+ android.net.InterfaceConfigurationParcel interfaceGetCfg(in @utf8InCpp String ifName);
+ void interfaceSetCfg(in android.net.InterfaceConfigurationParcel cfg);
+ void interfaceSetIPv6PrivacyExtensions(in @utf8InCpp String ifName, boolean enable);
+ void interfaceClearAddrs(in @utf8InCpp String ifName);
+ void interfaceSetEnableIPv6(in @utf8InCpp String ifName, boolean enable);
+ void interfaceSetMtu(in @utf8InCpp String ifName, int mtu);
+ void tetherAddForward(in @utf8InCpp String intIface, in @utf8InCpp String extIface);
+ void tetherRemoveForward(in @utf8InCpp String intIface, in @utf8InCpp String extIface);
+ void setTcpRWmemorySize(in @utf8InCpp String rmemValues, in @utf8InCpp String wmemValues);
+ void registerUnsolicitedEventListener(android.net.INetdUnsolicitedEventListener listener);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void firewallAddUidInterfaceRules(in @utf8InCpp String ifName, in int[] uids);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void firewallRemoveUidInterfaceRules(in int[] uids);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void trafficSwapActiveStatsMap();
+ IBinder getOemNetd();
+ void tetherStartWithConfiguration(in android.net.TetherConfigParcel config);
+ android.net.MarkMaskParcel getFwmarkForNetwork(int netId);
+ void networkAddRouteParcel(int netId, in android.net.RouteInfoParcel routeInfo);
+ void networkUpdateRouteParcel(int netId, in android.net.RouteInfoParcel routeInfo);
+ void networkRemoveRouteParcel(int netId, in android.net.RouteInfoParcel routeInfo);
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+ */
+ void tetherOffloadRuleAdd(in android.net.TetherOffloadRuleParcel rule);
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+ */
+ void tetherOffloadRuleRemove(in android.net.TetherOffloadRuleParcel rule);
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+ */
+ android.net.TetherStatsParcel[] tetherOffloadGetStats();
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+ */
+ void tetherOffloadSetInterfaceQuota(int ifIndex, long quotaBytes);
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+ */
+ android.net.TetherStatsParcel tetherOffloadGetAndClearStats(int ifIndex);
+ void networkCreate(in android.net.NativeNetworkConfig config);
+ void networkAddUidRangesParcel(in android.net.netd.aidl.NativeUidRangeConfig uidRangesConfig);
+ void networkRemoveUidRangesParcel(in android.net.netd.aidl.NativeUidRangeConfig uidRangesConfig);
+ void ipSecMigrate(in android.net.IpSecMigrateInfoParcel migrateInfo);
+ void setNetworkAllowlist(in android.net.netd.aidl.NativeUidRangeConfig[] allowedNetworks);
+ void networkAllowBypassVpnOnNetwork(boolean allow, int uid, int netId);
+ const int IPV4 = 4;
+ const int IPV6 = 6;
+ const int CONF = 1;
+ const int NEIGH = 2;
+ const String IPSEC_INTERFACE_PREFIX = "ipsec";
+ const int IPV6_ADDR_GEN_MODE_EUI64 = 0;
+ const int IPV6_ADDR_GEN_MODE_NONE = 1;
+ const int IPV6_ADDR_GEN_MODE_STABLE_PRIVACY = 2;
+ const int IPV6_ADDR_GEN_MODE_RANDOM = 3;
+ const int IPV6_ADDR_GEN_MODE_DEFAULT = 0;
+ const int PENALTY_POLICY_ACCEPT = 1;
+ const int PENALTY_POLICY_LOG = 2;
+ const int PENALTY_POLICY_REJECT = 3;
+ const int CLAT_MARK = 0xdeadc1a7;
+ const int LOCAL_NET_ID = 99;
+ const int DUMMY_NET_ID = 51;
+ const int UNREACHABLE_NET_ID = 52;
+ const String NEXTHOP_NONE = "";
+ const String NEXTHOP_UNREACHABLE = "unreachable";
+ const String NEXTHOP_THROW = "throw";
+ const int PERMISSION_NONE = 0;
+ const int PERMISSION_NETWORK = 1;
+ const int PERMISSION_SYSTEM = 2;
+ /**
+ * @deprecated usage is internal to module.
+ */
+ const int NO_PERMISSIONS = 0;
+ /**
+ * @deprecated usage is internal to module.
+ */
+ const int PERMISSION_INTERNET = 4;
+ /**
+ * @deprecated usage is internal to module.
+ */
+ const int PERMISSION_UPDATE_DEVICE_STATS = 8;
+ /**
+ * @deprecated usage is internal to module.
+ */
+ const int PERMISSION_UNINSTALLED = (-1) /* -1 */;
+ /**
+ * @deprecated use FIREWALL_ALLOWLIST.
+ */
+ const int FIREWALL_WHITELIST = 0;
+ const int FIREWALL_ALLOWLIST = 0;
+ /**
+ * @deprecated use FIREWALL_DENYLIST.
+ */
+ const int FIREWALL_BLACKLIST = 1;
+ const int FIREWALL_DENYLIST = 1;
+ const int FIREWALL_RULE_ALLOW = 1;
+ const int FIREWALL_RULE_DENY = 2;
+ const int FIREWALL_CHAIN_NONE = 0;
+ const int FIREWALL_CHAIN_DOZABLE = 1;
+ const int FIREWALL_CHAIN_STANDBY = 2;
+ const int FIREWALL_CHAIN_POWERSAVE = 3;
+ const int FIREWALL_CHAIN_RESTRICTED = 4;
+ const String IF_STATE_UP = "up";
+ const String IF_STATE_DOWN = "down";
+ const String IF_FLAG_BROADCAST = "broadcast";
+ const String IF_FLAG_LOOPBACK = "loopback";
+ const String IF_FLAG_POINTOPOINT = "point-to-point";
+ const String IF_FLAG_RUNNING = "running";
+ const String IF_FLAG_MULTICAST = "multicast";
+ const int IPSEC_DIRECTION_IN = 0;
+ const int IPSEC_DIRECTION_OUT = 1;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/INetdUnsolicitedEventListener.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/INetdUnsolicitedEventListener.aidl
new file mode 100644
index 0000000..31775df
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/INetdUnsolicitedEventListener.aidl
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2018, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetdUnsolicitedEventListener {
+ oneway void onInterfaceClassActivityChanged(boolean isActive, int timerLabel, long timestampNs, int uid);
+ oneway void onQuotaLimitReached(@utf8InCpp String alertName, @utf8InCpp String ifName);
+ oneway void onInterfaceDnsServerInfo(@utf8InCpp String ifName, long lifetimeS, in @utf8InCpp String[] servers);
+ oneway void onInterfaceAddressUpdated(@utf8InCpp String addr, @utf8InCpp String ifName, int flags, int scope);
+ oneway void onInterfaceAddressRemoved(@utf8InCpp String addr, @utf8InCpp String ifName, int flags, int scope);
+ oneway void onInterfaceAdded(@utf8InCpp String ifName);
+ oneway void onInterfaceRemoved(@utf8InCpp String ifName);
+ oneway void onInterfaceChanged(@utf8InCpp String ifName, boolean up);
+ oneway void onInterfaceLinkStateChanged(@utf8InCpp String ifName, boolean up);
+ oneway void onRouteChanged(boolean updated, @utf8InCpp String route, @utf8InCpp String gateway, @utf8InCpp String ifName);
+ oneway void onStrictCleartextDetected(int uid, @utf8InCpp String hex);
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/InterfaceConfigurationParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/InterfaceConfigurationParcel.aidl
new file mode 100644
index 0000000..1869d8d
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/InterfaceConfigurationParcel.aidl
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable InterfaceConfigurationParcel {
+ @utf8InCpp String ifName;
+ @utf8InCpp String hwAddr;
+ @utf8InCpp String ipv4Addr;
+ int prefixLength;
+ @utf8InCpp String[] flags;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/IpSecMigrateInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/IpSecMigrateInfoParcel.aidl
new file mode 100644
index 0000000..975a261
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/IpSecMigrateInfoParcel.aidl
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2022, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaOnlyImmutable
+parcelable IpSecMigrateInfoParcel {
+ int requestId;
+ int selAddrFamily;
+ int direction;
+ @utf8InCpp String oldSourceAddress;
+ @utf8InCpp String oldDestinationAddress;
+ @utf8InCpp String newSourceAddress;
+ @utf8InCpp String newDestinationAddress;
+ int interfaceId;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/MarkMaskParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/MarkMaskParcel.aidl
new file mode 100644
index 0000000..8ea20d1
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/MarkMaskParcel.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable MarkMaskParcel {
+ int mark;
+ int mask;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeNetworkConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeNetworkConfig.aidl
new file mode 100644
index 0000000..77d814b
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeNetworkConfig.aidl
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable NativeNetworkConfig {
+ int netId;
+ android.net.NativeNetworkType networkType = android.net.NativeNetworkType.PHYSICAL;
+ int permission;
+ boolean secure;
+ android.net.NativeVpnType vpnType = android.net.NativeVpnType.PLATFORM;
+ boolean excludeLocalRoutes = false;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeNetworkType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeNetworkType.aidl
new file mode 100644
index 0000000..e77a143
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeNetworkType.aidl
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@Backing(type="int")
+enum NativeNetworkType {
+ PHYSICAL = 0,
+ VIRTUAL = 1,
+ PHYSICAL_LOCAL = 2,
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeVpnType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeVpnType.aidl
new file mode 100644
index 0000000..8a8be83
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeVpnType.aidl
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@Backing(type="int")
+enum NativeVpnType {
+ SERVICE = 1,
+ PLATFORM = 2,
+ LEGACY = 3,
+ OEM = 4,
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/RouteInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/RouteInfoParcel.aidl
new file mode 100644
index 0000000..5ef95e6
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/RouteInfoParcel.aidl
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+parcelable RouteInfoParcel {
+ @utf8InCpp String destination;
+ @utf8InCpp String ifName;
+ @utf8InCpp String nextHop;
+ int mtu;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherConfigParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherConfigParcel.aidl
new file mode 100644
index 0000000..7b39c22
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherConfigParcel.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable TetherConfigParcel {
+ boolean usingLegacyDnsProxy;
+ @utf8InCpp String[] dhcpRanges;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherOffloadRuleParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherOffloadRuleParcel.aidl
new file mode 100644
index 0000000..983e986
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherOffloadRuleParcel.aidl
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable TetherOffloadRuleParcel {
+ int inputInterfaceIndex;
+ int outputInterfaceIndex;
+ byte[] destination;
+ int prefixLength;
+ byte[] srcL2Address;
+ byte[] dstL2Address;
+ int pmtu = 1500;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherStatsParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherStatsParcel.aidl
new file mode 100644
index 0000000..5f1b722
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherStatsParcel.aidl
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable TetherStatsParcel {
+ @utf8InCpp String iface;
+ long rxBytes;
+ long rxPackets;
+ long txBytes;
+ long txPackets;
+ int ifIndex = 0;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/UidRangeParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/UidRangeParcel.aidl
new file mode 100644
index 0000000..72e987a
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/UidRangeParcel.aidl
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable UidRangeParcel {
+ int start;
+ int stop;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/netd/aidl/NativeUidRangeConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/netd/aidl/NativeUidRangeConfig.aidl
new file mode 100644
index 0000000..9bb679f
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/netd/aidl/NativeUidRangeConfig.aidl
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.netd.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable NativeUidRangeConfig {
+ int netId;
+ android.net.UidRangeParcel[] uidRanges;
+ int subPriority;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/INetd.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/INetd.aidl
index 80b3b62..8351b56 100644
--- a/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/INetd.aidl
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/INetd.aidl
@@ -227,9 +227,21 @@
const int PERMISSION_NONE = 0;
const int PERMISSION_NETWORK = 1;
const int PERMISSION_SYSTEM = 2;
+ /**
+ * @deprecated usage is internal to module.
+ */
const int NO_PERMISSIONS = 0;
+ /**
+ * @deprecated usage is internal to module.
+ */
const int PERMISSION_INTERNET = 4;
+ /**
+ * @deprecated usage is internal to module.
+ */
const int PERMISSION_UPDATE_DEVICE_STATS = 8;
+ /**
+ * @deprecated usage is internal to module.
+ */
const int PERMISSION_UNINSTALLED = (-1) /* -1 */;
/**
* @deprecated use FIREWALL_ALLOWLIST.
diff --git a/staticlibs/netd/binder/android/net/INetd.aidl b/staticlibs/netd/binder/android/net/INetd.aidl
index e4c63b9..be8f538 100644
--- a/staticlibs/netd/binder/android/net/INetd.aidl
+++ b/staticlibs/netd/binder/android/net/INetd.aidl
@@ -933,24 +933,27 @@
/**
* NO_PERMISSIONS indicates that this app is installed and doesn't have either
* PERMISSION_INTERNET or PERMISSION_UPDATE_DEVICE_STATS.
- * TODO: use PERMISSION_NONE to represent this case
+ * @deprecated usage is internal to module.
*/
const int NO_PERMISSIONS = 0;
/**
- * PERMISSION_INTERNET indicates that the app can create AF_INET and AF_INET6 sockets
+ * PERMISSION_INTERNET indicates that the app can create AF_INET and AF_INET6 sockets.
+ * @deprecated usage is internal to module.
*/
const int PERMISSION_INTERNET = 4;
/**
* PERMISSION_UPDATE_DEVICE_STATS is used for system UIDs and privileged apps
- * that have the UPDATE_DEVICE_STATS permission
+ * that have the UPDATE_DEVICE_STATS permission.
+ * @deprecated usage is internal to module.
*/
const int PERMISSION_UPDATE_DEVICE_STATS = 8;
/**
* PERMISSION_UNINSTALLED is used when an app is uninstalled from the device. All internet
- * related permissions need to be cleaned
+ * related permissions need to be cleaned.
+ * @deprecated usage is internal to module.
*/
const int PERMISSION_UNINSTALLED = -1;
diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
index 9d1d291..f4f1ea9 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -28,6 +28,7 @@
"net-utils-device-common-struct-base",
"net-utils-device-common-wear",
"net-utils-service-connectivity",
+ "truth",
],
libs: [
"android.test.runner.stubs",
diff --git a/staticlibs/tests/unit/jni/Android.bp b/staticlibs/tests/unit/jni/Android.bp
index e456471..c444159 100644
--- a/staticlibs/tests/unit/jni/Android.bp
+++ b/staticlibs/tests/unit/jni/Android.bp
@@ -30,7 +30,7 @@
"com_android_net_moduletests_util/onload.cpp",
],
static_libs: [
- "libnet_utils_device_common_timerfdjni",
+ "libserviceconnectivityjni",
],
shared_libs: [
"liblog",
diff --git a/staticlibs/tests/unit/jni/com_android_net_moduletests_util/onload.cpp b/staticlibs/tests/unit/jni/com_android_net_moduletests_util/onload.cpp
index a035540..af4810f 100644
--- a/staticlibs/tests/unit/jni/com_android_net_moduletests_util/onload.cpp
+++ b/staticlibs/tests/unit/jni/com_android_net_moduletests_util/onload.cpp
@@ -22,7 +22,7 @@
namespace android {
-int register_com_android_net_module_util_TimerFdUtils(JNIEnv *env,
+int register_com_android_net_module_util_ServiceConnectivityJni(JNIEnv *env,
char const *class_name);
extern "C" jint JNI_OnLoad(JavaVM *vm, void *) {
@@ -32,8 +32,8 @@
return JNI_ERR;
}
- if (register_com_android_net_module_util_TimerFdUtils(
- env, "com/android/net/moduletests/util/TimerFdUtils") < 0)
+ if (register_com_android_net_module_util_ServiceConnectivityJni(
+ env, "com/android/net/moduletests/util/ServiceConnectivityJni") < 0)
return JNI_ERR;
return JNI_VERSION_1_6;
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/RealtimeSchedulerTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/RealtimeSchedulerTest.kt
new file mode 100644
index 0000000..30b530f
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/RealtimeSchedulerTest.kt
@@ -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.net.module.util
+
+import android.os.Build
+import android.os.ConditionVariable
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Looper
+import android.os.Message
+import android.os.SystemClock
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.tryTest
+import com.android.testutils.visibleOnHandlerThread
+import com.google.common.collect.Range
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertEquals
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class RealtimeSchedulerTest {
+
+ private val TIMEOUT_MS = 1000L
+ private val TOLERANCE_MS = 50L
+ private class TestHandler(looper: Looper) : Handler(looper) {
+ override fun handleMessage(msg: Message) {
+ val pair = msg.obj as Pair<ConditionVariable, MutableList<Long>>
+ val cv = pair.first
+ cv.open()
+ val executionTimes = pair.second
+ executionTimes.add(SystemClock.elapsedRealtime())
+ }
+ }
+ private val thread = HandlerThread(RealtimeSchedulerTest::class.simpleName).apply { start() }
+ private val handler by lazy { TestHandler(thread.looper) }
+
+ @After
+ fun tearDown() {
+ thread.quitSafely()
+ thread.join()
+ }
+
+ @Test
+ fun testMultiplePostDelayedTasks() {
+ val scheduler = RealtimeScheduler(handler)
+ tryTest {
+ val initialTimeMs = SystemClock.elapsedRealtime()
+ val executionTimes = mutableListOf<Long>()
+ val cv = ConditionVariable()
+ handler.post {
+ scheduler.postDelayed(
+ { executionTimes.add(SystemClock.elapsedRealtime() - initialTimeMs) }, 0)
+ scheduler.postDelayed(
+ { executionTimes.add(SystemClock.elapsedRealtime() - initialTimeMs) }, 200)
+ val toBeRemoved = Runnable {
+ executionTimes.add(SystemClock.elapsedRealtime() - initialTimeMs)
+ }
+ scheduler.postDelayed(toBeRemoved, 250)
+ scheduler.removeDelayedRunnable(toBeRemoved)
+ scheduler.postDelayed(
+ { executionTimes.add(SystemClock.elapsedRealtime() - initialTimeMs) }, 100)
+ scheduler.postDelayed({
+ executionTimes.add(SystemClock.elapsedRealtime() - initialTimeMs)
+ cv.open() }, 300)
+ }
+ cv.block(TIMEOUT_MS)
+ assertEquals(4, executionTimes.size)
+ assertThat(executionTimes[0]).isIn(Range.closed(0L, TOLERANCE_MS))
+ assertThat(executionTimes[1]).isIn(Range.closed(100L, 100 + TOLERANCE_MS))
+ assertThat(executionTimes[2]).isIn(Range.closed(200L, 200 + TOLERANCE_MS))
+ assertThat(executionTimes[3]).isIn(Range.closed(300L, 300 + TOLERANCE_MS))
+ } cleanup {
+ visibleOnHandlerThread(handler) { scheduler.close() }
+ }
+ }
+
+ @Test
+ fun testMultipleSendDelayedMessages() {
+ val scheduler = RealtimeScheduler(handler)
+ tryTest {
+ val MSG_ID_0 = 0
+ val MSG_ID_1 = 1
+ val MSG_ID_2 = 2
+ val MSG_ID_3 = 3
+ val MSG_ID_4 = 4
+ val initialTimeMs = SystemClock.elapsedRealtime()
+ val executionTimes = mutableListOf<Long>()
+ val cv = ConditionVariable()
+ handler.post {
+ scheduler.sendDelayedMessage(
+ Message.obtain(handler, MSG_ID_0, Pair(ConditionVariable(), executionTimes)), 0)
+ scheduler.sendDelayedMessage(
+ Message.obtain(handler, MSG_ID_1, Pair(ConditionVariable(), executionTimes)),
+ 200)
+ scheduler.sendDelayedMessage(
+ Message.obtain(handler, MSG_ID_4, Pair(ConditionVariable(), executionTimes)),
+ 250)
+ scheduler.removeDelayedMessage(MSG_ID_4)
+ scheduler.sendDelayedMessage(
+ Message.obtain(handler, MSG_ID_2, Pair(ConditionVariable(), executionTimes)),
+ 100)
+ scheduler.sendDelayedMessage(
+ Message.obtain(handler, MSG_ID_3, Pair(cv, executionTimes)),
+ 300)
+ }
+ cv.block(TIMEOUT_MS)
+ assertEquals(4, executionTimes.size)
+ assertThat(executionTimes[0] - initialTimeMs).isIn(Range.closed(0L, TOLERANCE_MS))
+ assertThat(executionTimes[1] - initialTimeMs)
+ .isIn(Range.closed(100L, 100 + TOLERANCE_MS))
+ assertThat(executionTimes[2] - initialTimeMs)
+ .isIn(Range.closed(200L, 200 + TOLERANCE_MS))
+ assertThat(executionTimes[3] - initialTimeMs)
+ .isIn(Range.closed(300L, 300 + TOLERANCE_MS))
+ } cleanup {
+ visibleOnHandlerThread(handler) { scheduler.close() }
+ }
+ }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/TerribleErrorLogTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/TerribleErrorLogTest.kt
new file mode 100644
index 0000000..5fd634e
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/TerribleErrorLogTest.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util
+
+import android.util.Log
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.testutils.tryTest
+import kotlin.test.assertContentEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class TerribleErrorLogTest {
+ @Test
+ fun testLogTerribleError() {
+ val wtfCaptures = mutableListOf<String>()
+ val prevHandler = Log.setWtfHandler { tag, what, system ->
+ wtfCaptures.add("$tag,${what.message}")
+ }
+ val statsLogCapture = mutableListOf<Pair<Int, Int>>()
+ val testStatsLog = object {
+ fun write(protoType: Int, errorType: Int) {
+ statsLogCapture.add(protoType to errorType)
+ }
+ }
+ tryTest {
+ TerribleErrorLog.logTerribleError(testStatsLog::write, "error", 1, 2)
+ assertContentEquals(listOf(1 to 2), statsLogCapture)
+ assertContentEquals(listOf("TerribleErrorLog,error"), wtfCaptures)
+ } cleanup {
+ Log.setWtfHandler(prevHandler)
+ }
+ }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/TimerFileDescriptorTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/TimerFileDescriptorTest.kt
deleted file mode 100644
index f5e47c9..0000000
--- a/staticlibs/tests/unit/src/com/android/net/module/util/TimerFileDescriptorTest.kt
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.net.module.util
-
-import android.os.Build
-import android.os.ConditionVariable
-import android.os.Handler
-import android.os.HandlerThread
-import android.os.Looper
-import android.os.Message
-import androidx.test.filters.SmallTest
-import com.android.net.module.util.TimerFileDescriptor.ITask
-import com.android.net.module.util.TimerFileDescriptor.MessageTask
-import com.android.net.module.util.TimerFileDescriptor.RunnableTask
-import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.DevSdkIgnoreRunner
-import com.android.testutils.tryTest
-import com.android.testutils.visibleOnHandlerThread
-import org.junit.After
-import org.junit.Test
-import org.junit.runner.RunWith
-import java.time.Duration
-import java.time.Instant
-import kotlin.test.assertFalse
-import kotlin.test.assertTrue
-
-private const val MSG_TEST = 1
-
-@DevSdkIgnoreRunner.MonitorThreadLeak
-@RunWith(DevSdkIgnoreRunner::class)
-@SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
-class TimerFileDescriptorTest {
- private class TestHandler(looper: Looper) : Handler(looper) {
- override fun handleMessage(msg: Message) {
- val cv = msg.obj as ConditionVariable
- cv.open()
- }
- }
- private val thread = HandlerThread(TimerFileDescriptorTest::class.simpleName).apply { start() }
- private val handler by lazy { TestHandler(thread.looper) }
-
- @After
- fun tearDown() {
- thread.quitSafely()
- thread.join()
- }
-
- private fun assertDelayedTaskPost(
- timerFd: TimerFileDescriptor,
- task: ITask,
- cv: ConditionVariable
- ) {
- val delayTime = 10L
- val startTime1 = Instant.now()
- handler.post { timerFd.setDelayedTask(task, delayTime) }
- assertTrue(cv.block(100L /* timeoutMs*/))
- assertTrue(Duration.between(startTime1, Instant.now()).toMillis() >= delayTime)
- }
-
- @Test
- fun testSetDelayedTask() {
- val timerFd = TimerFileDescriptor(handler)
- tryTest {
- // Verify the delayed task is executed with the self-implemented ITask
- val cv1 = ConditionVariable()
- assertDelayedTaskPost(timerFd, { cv1.open() }, cv1)
-
- // Verify the delayed task is executed with the RunnableTask
- val cv2 = ConditionVariable()
- assertDelayedTaskPost(timerFd, RunnableTask{ cv2.open() }, cv2)
-
- // Verify the delayed task is executed with the MessageTask
- val cv3 = ConditionVariable()
- assertDelayedTaskPost(timerFd, MessageTask(handler.obtainMessage(MSG_TEST, cv3)), cv3)
- } cleanup {
- visibleOnHandlerThread(handler) { timerFd.close() }
- }
- }
-
- @Test
- fun testCancelTask() {
- // The task is posted and canceled within the same handler loop, so the short delay used
- // here won't cause flakes.
- val delayTime = 10L
- val timerFd = TimerFileDescriptor(handler)
- val cv = ConditionVariable()
- tryTest {
- handler.post {
- timerFd.setDelayedTask({ cv.open() }, delayTime)
- assertTrue(timerFd.hasDelayedTask())
- timerFd.cancelTask()
- assertFalse(timerFd.hasDelayedTask())
- }
- assertFalse(cv.block(20L /* timeoutMs*/))
- } cleanup {
- visibleOnHandlerThread(handler) { timerFd.close() }
- }
- }
-}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
index 8104e3a..b29fc73 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
@@ -308,7 +308,7 @@
@Test
public void testCreateSetInterfaceFlagsMessage() {
final String expectedHexBytes =
- "20000000100001006824000000000000" // struct nlmsghdr
+ "20000000100005006824000000000000" // struct nlmsghdr
+ "00000000080000000100000001000100"; // struct ifinfomsg
final String interfaceName = "wlan0";
final int interfaceIndex = 8;
diff --git a/staticlibs/testutils/Android.bp b/staticlibs/testutils/Android.bp
index 86aa8f1..ec486fb 100644
--- a/staticlibs/testutils/Android.bp
+++ b/staticlibs/testutils/Android.bp
@@ -93,6 +93,7 @@
libs: ["tradefed"],
test_suites: [
"ats",
+ "automotive-general-tests",
"device-tests",
"general-tests",
"cts",
diff --git a/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
index 8e27c62..c42d9e5 100644
--- a/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
+++ b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
@@ -19,7 +19,7 @@
import android.Manifest.permission.NETWORK_SETTINGS
import android.content.pm.PackageManager.FEATURE_TELEPHONY
import android.content.pm.PackageManager.FEATURE_WIFI
-import android.net.LinkAddress
+import android.net.InetAddresses.parseNumericAddress
import android.net.Network
import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
import android.net.NetworkCapabilities.TRANSPORT_WIFI
@@ -66,7 +66,8 @@
// Skip IPv6 checks on virtual devices which do not support it. Tests that require IPv6 will
// still fail even if the preparer does not.
private fun ipv6Unsupported(wifiSsid: String?) = ConnectUtil.VIRTUAL_SSIDS.contains(
- WifiInfo.sanitizeSsid(wifiSsid))
+ WifiInfo.sanitizeSsid(wifiSsid)
+ )
@Test
fun testCheckWifiSetup() {
@@ -89,13 +90,25 @@
pos = 0,
timeoutMs = 30_000L
) {
- it is LinkPropertiesChanged &&
- it.network == network &&
- it.lp.allLinkAddresses.any(LinkAddress::isIpv4) &&
- (ipv6Unsupported(ssid) || it.lp.hasGlobalIpv6Address())
+ if (it !is LinkPropertiesChanged || it.network != network) {
+ false
+ } else {
+ // Same check as used by DnsResolver for AI_ADDRCONFIG (have_ipv4)
+ val ipv4Reachable = it.lp.isReachable(parseNumericAddress("8.8.8.8"))
+ // Same check as used by DnsResolver for AI_ADDRCONFIG (have_ipv6)
+ val ipv6Reachable = it.lp.isReachable(parseNumericAddress("2000::"))
+ ipv4Reachable && (ipv6Unsupported(ssid) || ipv6Reachable)
+ }
}
- assertNotNull(lpChange, "Wifi network $network needs an IPv4 address" +
- if (ipv6Unsupported(ssid)) "" else " and a global IPv6 address")
+ assertNotNull(
+ lpChange,
+ "Wifi network $network needs an IPv4 address and default route" +
+ if (ipv6Unsupported(ssid)) {
+ ""
+ } else {
+ " and a global IPv6 address and default route"
+ }
+ )
Pair(network, ssid)
}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/CarrierConfigRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/CarrierConfigRule.kt
new file mode 100644
index 0000000..ae0de79
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/CarrierConfigRule.kt
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils.com.android.testutils
+
+import android.Manifest.permission.MODIFY_PHONE_STATE
+import android.Manifest.permission.READ_PHONE_STATE
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.ConditionVariable
+import android.os.PersistableBundle
+import android.telephony.CarrierConfigManager
+import android.telephony.CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED
+import android.telephony.SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX
+import android.util.Log
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.modules.utils.build.SdkLevel.isAtLeastU
+import com.android.testutils.runAsShell
+import com.android.testutils.tryTest
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+private val TAG = CarrierConfigRule::class.simpleName
+private const val CARRIER_CONFIG_CHANGE_TIMEOUT_MS = 10_000L
+
+/**
+ * A [TestRule] that helps set [CarrierConfigManager] overrides for tests and clean up the test
+ * configuration automatically on teardown.
+ */
+class CarrierConfigRule : TestRule {
+ private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
+ private val ccm by lazy { context.getSystemService(CarrierConfigManager::class.java) }
+
+ // Map of (subId) -> (original values of overridden settings)
+ private val originalConfigs = mutableMapOf<Int, PersistableBundle>()
+
+ override fun apply(base: Statement, description: Description): Statement {
+ return CarrierConfigStatement(base, description)
+ }
+
+ private inner class CarrierConfigStatement(
+ private val base: Statement,
+ private val description: Description
+ ) : Statement() {
+ override fun evaluate() {
+ tryTest {
+ base.evaluate()
+ } cleanup {
+ cleanUpNow()
+ }
+ }
+ }
+
+ private class ConfigChangeReceiver(private val subId: Int) : BroadcastReceiver() {
+ val cv = ConditionVariable()
+ override fun onReceive(context: Context, intent: Intent) {
+ if (intent.action != ACTION_CARRIER_CONFIG_CHANGED ||
+ intent.getIntExtra(EXTRA_SUBSCRIPTION_INDEX, -1) != subId) {
+ return
+ }
+ // This may race with other config changes for the same subId, but there is no way to
+ // know which update is being reported, and querying the override would return the
+ // latest values even before the config is applied. Config changes should be rare, so it
+ // is unlikely they would happen exactly after the override applied here and cause
+ // flakes.
+ cv.open()
+ }
+ }
+
+ private fun overrideConfigAndWait(subId: Int, config: PersistableBundle) {
+ val changeReceiver = ConfigChangeReceiver(subId)
+ context.registerReceiver(changeReceiver, IntentFilter(ACTION_CARRIER_CONFIG_CHANGED))
+ ccm.overrideConfig(subId, config)
+ assertTrue(
+ changeReceiver.cv.block(CARRIER_CONFIG_CHANGE_TIMEOUT_MS),
+ "Timed out waiting for config change for subId $subId"
+ )
+ context.unregisterReceiver(changeReceiver)
+ }
+
+ /**
+ * Add carrier config overrides with the specified configuration.
+ *
+ * The overrides will automatically be cleaned up when the test case finishes.
+ */
+ fun addConfigOverrides(subId: Int, config: PersistableBundle) {
+ val originalConfig = originalConfigs.computeIfAbsent(subId) { PersistableBundle() }
+ val overrideKeys = config.keySet()
+ val previousValues = runAsShell(READ_PHONE_STATE) {
+ ccm.getConfigForSubIdCompat(subId, overrideKeys)
+ }
+ // If a key is already in the originalConfig, keep the oldest original overrides
+ originalConfig.keySet().forEach {
+ previousValues.remove(it)
+ }
+ originalConfig.putAll(previousValues)
+
+ runAsShell(MODIFY_PHONE_STATE) {
+ overrideConfigAndWait(subId, config)
+ }
+ }
+
+ /**
+ * Cleanup overrides that were added by the test case.
+ *
+ * This will be called automatically on test teardown, so it does not need to be called by the
+ * test case unless cleaning up earlier is required.
+ */
+ fun cleanUpNow() {
+ runAsShell(MODIFY_PHONE_STATE) {
+ originalConfigs.forEach { (subId, config) ->
+ try {
+ // Do not use null as the config to reset, as it would reset configs that may
+ // have been set by target preparers such as
+ // ConnectivityTestTargetPreparer / CarrierConfigSetupTest.
+ overrideConfigAndWait(subId, config)
+ } catch (e: Throwable) {
+ Log.e(TAG, "Error resetting carrier config for subId $subId")
+ }
+ }
+ originalConfigs.clear()
+ }
+ }
+}
+
+private fun CarrierConfigManager.getConfigForSubIdCompat(
+ subId: Int,
+ keys: Set<String>
+): PersistableBundle {
+ return if (isAtLeastU()) {
+ // This method is U+
+ getConfigForSubId(subId, *keys.toTypedArray())
+ } else {
+ @Suppress("DEPRECATION")
+ val config = assertNotNull(getConfigForSubId(subId))
+ val allKeys = config.keySet().toList()
+ allKeys.forEach {
+ if (!keys.contains(it)) {
+ config.remove(it)
+ }
+ }
+ config
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt b/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
index 0624e5f..c7d6850 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
@@ -168,7 +168,8 @@
.addCapability(NET_CAPABILITY_INTERNET)
.addTransportType(TRANSPORT_WIFI)
.addTransportType(TRANSPORT_CELLULAR)
- .build(), networkCallback
+ .build(),
+ networkCallback
)
}
}
@@ -184,9 +185,12 @@
// when iterating on failing tests.
if (!runOnFailure(failure.exception)) return
if (outputFiles.size >= MAX_DUMPS) return
- Log.i(TAG, "Collecting diagnostics for test failure. Disable by running tests with: " +
+ Log.i(
+ TAG,
+ "Collecting diagnostics for test failure. Disable by running tests with: " +
"atest MyModule -- " +
- "--module-arg MyModule:instrumentation-arg:$ARG_RUN_ON_FAILURE:=false")
+ "--module-arg MyModule:instrumentation-arg:$ARG_RUN_ON_FAILURE:=false"
+ )
collectTestFailureDiagnostics(failure.exception)
val baseFilename = "${description.className}#${description.methodName}_failure"
@@ -326,8 +330,11 @@
}
}
} else {
- Log.w(TAG, "The test is still holding shell permissions, cannot collect privileged " +
- "device info")
+ Log.w(
+ TAG,
+ "The test is still holding shell permissions, cannot collect privileged " +
+ "device info"
+ )
headerObj.put("shellPermissionsUnavailable", true)
}
failureHeader = headerObj.apply {
@@ -379,7 +386,9 @@
cbHelper.registerNetworkCallback(
NetworkRequest.Builder()
.addTransportType(TRANSPORT_WIFI)
- .addCapability(NET_CAPABILITY_INTERNET).build(), cb)
+ .addCapability(NET_CAPABILITY_INTERNET).build(),
+ cb
+ )
return try {
cb.wifiInfoFuture.get(1L, TimeUnit.SECONDS)
} catch (e: TimeoutException) {
@@ -410,15 +419,29 @@
* @param exceptionContext An exception to write a stacktrace to the dump for context.
*/
fun collectDumpsysConnectivity(exceptionContext: Throwable? = null) {
- Log.i(TAG, "Collecting dumpsys connectivity for test artifacts")
+ collectDumpsys("connectivity --dump-priority HIGH", exceptionContext)
+ }
+
+ /**
+ * Add a dumpsys to the test data dump.
+ *
+ * <p>The dump will be collected immediately, and exported to a test artifact file when the
+ * test ends.
+ * @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")
PrintWriter(buffer).let {
- it.println("--- Dumpsys connectivity at ${ZonedDateTime.now()} ---")
+ it.println("--- Dumpsys $dumpsysCmd at ${ZonedDateTime.now()} ---")
maybeWriteExceptionContext(it, exceptionContext)
it.flush()
}
ParcelFileDescriptor.AutoCloseInputStream(
InstrumentationRegistry.getInstrumentation().uiAutomation.executeShellCommand(
- "dumpsys connectivity --dump-priority HIGH")).use {
+ "dumpsys $dumpsysCmd"
+ )
+ ).use {
it.copyTo(buffer)
}
}
@@ -437,4 +460,4 @@
writer.println("At: ")
exceptionContext.printStackTrace(writer)
}
-}
\ No newline at end of file
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt
index 8dc1bc4..bfbbc34 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt
@@ -14,19 +14,34 @@
* limitations under the License.
*/
-package com.android.testutils;
+package com.android.testutils
import android.content.Context
+import android.net.InetAddresses.parseNumericAddress
import android.net.KeepalivePacketData
+import android.net.LinkAddress
import android.net.LinkProperties
import android.net.NetworkAgent
import android.net.NetworkAgentConfig
import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
+import android.net.NetworkCapabilities.TRANSPORT_TEST
import android.net.NetworkProvider
+import android.net.NetworkRequest
import android.net.QosFilter
import android.net.Uri
import android.os.Looper
+import android.system.ErrnoException
+import android.system.Os
+import android.system.OsConstants
+import android.system.OsConstants.EADDRNOTAVAIL
+import android.system.OsConstants.ENETUNREACH
+import android.system.OsConstants.ENONET
+import android.system.OsConstants.IPPROTO_UDP
+import android.system.OsConstants.SOCK_DGRAM
+import com.android.modules.utils.build.SdkLevel.isAtLeastS
import com.android.net.module.util.ArrayTrackRecord
+import com.android.testutils.CompatUtil.makeTestNetworkSpecifier
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnAddKeepalivePacketFilter
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnAutomaticReconnectDisabled
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnBandwidthUpdateRequested
@@ -42,6 +57,8 @@
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnStopSocketKeepalive
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnUnregisterQosCallback
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnValidationStatus
+import java.net.NetworkInterface
+import java.net.SocketException
import java.time.Duration
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
@@ -65,6 +82,92 @@
conf: NetworkAgentConfig
) : NetworkAgent(context, looper, TestableNetworkAgent::class.java.simpleName /* tag */,
nc, lp, TEST_NETWORK_SCORE, conf, Provider(context, looper)) {
+ companion object {
+
+ /**
+ * Convenience method to create a [NetworkRequest] matching [TestableNetworkAgent]s from
+ * [createOnInterface].
+ */
+ fun makeNetworkRequestForInterface(ifaceName: String) = NetworkRequest.Builder()
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .addTransportType(TRANSPORT_TEST)
+ .setNetworkSpecifier(makeTestNetworkSpecifier(ifaceName))
+ .build()
+
+ /**
+ * Convenience method to initialize a [TestableNetworkAgent] on a given interface.
+ *
+ * This waits for link-local addresses to be setup and ensures LinkProperties are updated
+ * with the addresses.
+ */
+ fun createOnInterface(
+ context: Context,
+ looper: Looper,
+ ifaceName: String,
+ timeoutMs: Long
+ ): TestableNetworkAgent {
+ val lp = LinkProperties().apply {
+ interfaceName = ifaceName
+ }
+ val agent = TestableNetworkAgent(
+ context,
+ looper,
+ NetworkCapabilities().apply {
+ removeCapability(NET_CAPABILITY_TRUSTED)
+ addTransportType(TRANSPORT_TEST)
+ setNetworkSpecifier(makeTestNetworkSpecifier(ifaceName))
+ },
+ lp,
+ NetworkAgentConfig.Builder().build()
+ )
+ val network = agent.register()
+ agent.markConnected()
+ if (isAtLeastS()) {
+ // OnNetworkCreated was added in S
+ agent.eventuallyExpect<OnNetworkCreated>()
+ }
+
+ // Wait until the link-local address can be used. Address flags are not available
+ // without elevated permissions, so check that bindSocket works.
+ assertEventuallyTrue("No usable v6 address after $timeoutMs ms", timeoutMs) {
+ // To avoid race condition between socket connection succeeding and interface
+ // returning a non-empty address list. Verify that interface returns a non-empty
+ // list, before trying the socket connection.
+ if (NetworkInterface.getByName(ifaceName).interfaceAddresses.isEmpty()) {
+ return@assertEventuallyTrue false
+ }
+
+ val sock = Os.socket(OsConstants.AF_INET6, SOCK_DGRAM, IPPROTO_UDP)
+ tryTest {
+ network.bindSocket(sock)
+ Os.connect(sock, parseNumericAddress("ff02::fb%$ifaceName"), 12345)
+ true
+ }.catch<ErrnoException> {
+ if (it.errno != ENETUNREACH && it.errno != EADDRNOTAVAIL) {
+ throw it
+ }
+ false
+ }.catch<SocketException> {
+ // OnNetworkCreated does not exist on R, so a SocketException caused by ENONET
+ // may be seen before the network is created
+ if (isAtLeastS()) throw it
+ val cause = it.cause as? ErrnoException ?: throw it
+ if (cause.errno != ENONET) {
+ throw it
+ }
+ false
+ } cleanup {
+ Os.close(sock)
+ }
+ }
+
+ agent.lp.setLinkAddresses(NetworkInterface.getByName(ifaceName).interfaceAddresses.map {
+ LinkAddress(it.address, it.networkPrefixLength.toInt())
+ })
+ agent.sendLinkProperties(agent.lp)
+ return agent
+ }
+ }
val DEFAULT_TIMEOUT_MS = 5000L
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java
index ad98a29..ac60b0f 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java
@@ -25,6 +25,7 @@
import android.content.Context;
import android.net.TetheringInterface;
import android.net.cts.util.CtsTetheringUtils;
+import android.net.cts.util.CtsTetheringUtils.TestTetheringEventCallback;
import android.net.wifi.SoftApConfiguration;
import android.net.wifi.WifiSsid;
@@ -37,6 +38,7 @@
public class TetheringTest {
private CtsTetheringUtils mCtsTetheringUtils;
private TetheringHelperClient mTetheringHelperClient;
+ private TestTetheringEventCallback mTetheringEventCallback;
@Before
public void setUp() throws Exception {
@@ -44,11 +46,14 @@
mCtsTetheringUtils = new CtsTetheringUtils(targetContext);
mTetheringHelperClient = new TetheringHelperClient(targetContext);
mTetheringHelperClient.bind();
+ mTetheringEventCallback = mCtsTetheringUtils.registerTetheringEventCallback();
}
@After
public void tearDown() throws Exception {
mTetheringHelperClient.unbind();
+ mCtsTetheringUtils.unregisterTetheringEventCallback(mTetheringEventCallback);
+ mCtsTetheringUtils.stopAllTethering();
}
/**
@@ -57,24 +62,20 @@
*/
@Test
public void testSoftApConfigurationRedactedForOtherUids() throws Exception {
- final CtsTetheringUtils.TestTetheringEventCallback tetherEventCallback =
- mCtsTetheringUtils.registerTetheringEventCallback();
+ mTetheringEventCallback.assumeWifiTetheringSupported(
+ getInstrumentation().getTargetContext());
SoftApConfiguration softApConfig = new SoftApConfiguration.Builder()
.setWifiSsid(WifiSsid.fromBytes("This is an SSID!"
.getBytes(StandardCharsets.UTF_8))).build();
final TetheringInterface tetheringInterface =
- mCtsTetheringUtils.startWifiTethering(tetherEventCallback, softApConfig);
+ mCtsTetheringUtils.startWifiTethering(mTetheringEventCallback, softApConfig);
assertNotNull(tetheringInterface);
assertEquals(softApConfig, tetheringInterface.getSoftApConfiguration());
- try {
- TetheringInterface tetheringInterfaceForApp2 =
- mTetheringHelperClient.getTetheredWifiInterface();
- assertNotNull(tetheringInterfaceForApp2);
- assertNull(tetheringInterfaceForApp2.getSoftApConfiguration());
- assertEquals(
- tetheringInterface.getInterface(), tetheringInterfaceForApp2.getInterface());
- } finally {
- mCtsTetheringUtils.stopWifiTethering(tetherEventCallback);
- }
+ TetheringInterface tetheringInterfaceForApp2 =
+ mTetheringHelperClient.getTetheredWifiInterface();
+ assertNotNull(tetheringInterfaceForApp2);
+ assertNull(tetheringInterfaceForApp2.getSoftApConfiguration());
+ assertEquals(
+ tetheringInterface.getInterface(), tetheringInterfaceForApp2.getInterface());
}
}
diff --git a/tests/cts/multidevices/Android.bp b/tests/cts/multidevices/Android.bp
index 949be85..a082a95 100644
--- a/tests/cts/multidevices/Android.bp
+++ b/tests/cts/multidevices/Android.bp
@@ -22,6 +22,7 @@
main: "run_tests.py",
srcs: [
"apfv4_test.py",
+ "apfv6_test.py",
"connectivity_multi_devices_test.py",
"run_tests.py",
],
diff --git a/tests/cts/multidevices/apfv4_test.py b/tests/cts/multidevices/apfv4_test.py
index 7795be5..aa535fd 100644
--- a/tests/cts/multidevices/apfv4_test.py
+++ b/tests/cts/multidevices/apfv4_test.py
@@ -53,7 +53,7 @@
) # Declare inputs for state_str and expected_result.
def test_apf_drop_ethertype_not_allowed(self, blocked_ether_type):
# Ethernet header (14 bytes).
- packet = ETHER_BROADCAST_ADDR # Destination MAC (broadcast)
+ packet = self.client_mac_address.replace(":", "") # Destination MAC
packet += self.server_mac_address.replace(":", "") # Source MAC
packet += blocked_ether_type
diff --git a/tests/cts/multidevices/apfv6_test.py b/tests/cts/multidevices/apfv6_test.py
new file mode 100644
index 0000000..fc732d2
--- /dev/null
+++ b/tests/cts/multidevices/apfv6_test.py
@@ -0,0 +1,84 @@
+# 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.
+
+from mobly import asserts
+from net_tests_utils.host.python import apf_test_base, apf_utils, adb_utils, assert_utils, packet_utils
+
+APFV6_VERSION = 6000
+ARP_OFFLOAD_REPLY_LEN = 60
+
+class ApfV6Test(apf_test_base.ApfTestBase):
+ def setup_class(self):
+ super().setup_class()
+
+ # Skip tests for APF version < 6000
+ apf_utils.assume_apf_version_support_at_least(
+ self.clientDevice, self.client_iface_name, APFV6_VERSION
+ )
+
+ def teardown_class(self):
+ # force to stop capture on the server device if any test case failed
+ try:
+ apf_utils.stop_capture_packets(self.serverDevice, self.server_iface_name)
+ except assert_utils.UnexpectedBehaviorError:
+ pass
+ super().teardown_class()
+
+ def test_unicast_arp_request_offload(self):
+ arp_request = packet_utils.construct_arp_packet(
+ src_mac=self.server_mac_address,
+ dst_mac=self.client_mac_address,
+ src_ip=self.server_ipv4_addresses[0],
+ dst_ip=self.client_ipv4_addresses[0],
+ op=packet_utils.ARP_REQUEST_OP
+ )
+
+ arp_reply = packet_utils.construct_arp_packet(
+ src_mac=self.client_mac_address,
+ dst_mac=self.server_mac_address,
+ src_ip=self.client_ipv4_addresses[0],
+ dst_ip=self.server_ipv4_addresses[0],
+ op=packet_utils.ARP_REPLY_OP
+ )
+
+ # Add zero padding up to 60 bytes, since APFv6 ARP offload always sent out 60 bytes reply
+ arp_reply = arp_reply.ljust(ARP_OFFLOAD_REPLY_LEN * 2, "0")
+
+ self.send_packet_and_expect_reply_received(
+ arp_request, "DROPPED_ARP_REQUEST_REPLIED", arp_reply
+ )
+
+ def test_broadcast_arp_request_offload(self):
+ arp_request = packet_utils.construct_arp_packet(
+ src_mac=self.server_mac_address,
+ dst_mac=packet_utils.ETHER_BROADCAST_MAC_ADDRESS,
+ src_ip=self.server_ipv4_addresses[0],
+ dst_ip=self.client_ipv4_addresses[0],
+ op=packet_utils.ARP_REQUEST_OP
+ )
+
+ arp_reply = packet_utils.construct_arp_packet(
+ src_mac=self.client_mac_address,
+ dst_mac=self.server_mac_address,
+ src_ip=self.client_ipv4_addresses[0],
+ dst_ip=self.server_ipv4_addresses[0],
+ op=packet_utils.ARP_REPLY_OP
+ )
+
+ # Add zero padding up to 60 bytes, since APFv6 ARP offload always sent out 60 bytes reply
+ arp_reply = arp_reply.ljust(ARP_OFFLOAD_REPLY_LEN * 2, "0")
+
+ self.send_packet_and_expect_reply_received(
+ arp_request, "DROPPED_ARP_REQUEST_REPLIED", arp_reply
+ )
diff --git a/tests/cts/multidevices/run_tests.py b/tests/cts/multidevices/run_tests.py
index 1391d13..a0d0bec 100644
--- a/tests/cts/multidevices/run_tests.py
+++ b/tests/cts/multidevices/run_tests.py
@@ -16,6 +16,7 @@
import sys
from apfv4_test import ApfV4Test
+from apfv6_test import ApfV6Test
from connectivity_multi_devices_test import ConnectivityMultiDevicesTest
from mobly import suite_runner
@@ -35,4 +36,4 @@
index = sys.argv.index("--")
sys.argv = sys.argv[:1] + sys.argv[index + 1 :]
# TODO: make the tests can be executed without manually list classes.
- suite_runner.run_suite([ConnectivityMultiDevicesTest, ApfV4Test], sys.argv)
+ suite_runner.run_suite([ConnectivityMultiDevicesTest, ApfV4Test, ApfV6Test], sys.argv)
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index a9ac29c..1ba581a 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -95,6 +95,7 @@
"NetworkStackApiCurrentShims",
],
test_suites: [
+ "automotive-general-tests",
"cts",
"mts-tethering",
"mcts-tethering",
@@ -160,6 +161,7 @@
min_sdk_version: "30",
// Tag this module as a cts test artifact
test_suites: [
+ "automotive-general-tests",
"cts",
"general-tests",
],
diff --git a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
index 320622b..9379697 100644
--- a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
+++ b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
@@ -21,8 +21,8 @@
import android.Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG
import android.Manifest.permission.WRITE_DEVICE_CONFIG
-import android.content.pm.PackageManager
import android.content.pm.PackageManager.FEATURE_AUTOMOTIVE
+import android.content.pm.PackageManager.FEATURE_LEANBACK
import android.content.pm.PackageManager.FEATURE_WIFI
import android.net.ConnectivityManager
import android.net.Network
@@ -38,7 +38,7 @@
import android.net.apf.ApfConstants.IPV6_NEXT_HEADER_OFFSET
import android.net.apf.ApfConstants.IPV6_SRC_ADDR_OFFSET
import android.net.apf.ApfCounterTracker
-import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_MULTICAST_PING
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_NS_REPLIED_NON_DAD
import android.net.apf.ApfCounterTracker.Counter.FILTER_AGE_16384THS
import android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_ICMP
import android.net.apf.ApfV4Generator
@@ -53,6 +53,7 @@
import android.os.HandlerThread
import android.os.PowerManager
import android.os.UserManager
+import android.os.SystemProperties
import android.platform.test.annotations.AppModeFull
import android.provider.DeviceConfig
import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY
@@ -60,6 +61,8 @@
import android.system.OsConstants
import android.system.OsConstants.AF_INET6
import android.system.OsConstants.ETH_P_IPV6
+import android.system.OsConstants.ICMP6_ECHO_REPLY
+import android.system.OsConstants.ICMP6_ECHO_REQUEST
import android.system.OsConstants.IPPROTO_ICMPV6
import android.system.OsConstants.SOCK_DGRAM
import android.system.OsConstants.SOCK_NONBLOCK
@@ -104,6 +107,7 @@
import kotlin.test.assertNotNull
import org.junit.After
import org.junit.AfterClass
+import org.junit.Assume.assumeFalse
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Rule
@@ -170,8 +174,8 @@
private fun isAutomotiveWithVisibleBackgroundUser(): Boolean {
val packageManager = context.getPackageManager()
val userManager = context.getSystemService(UserManager::class.java)!!
- return (packageManager.hasSystemFeature(FEATURE_AUTOMOTIVE)
- && userManager.isVisibleBackgroundUsersSupported)
+ return (packageManager.hasSystemFeature(FEATURE_AUTOMOTIVE) &&
+ userManager.isVisibleBackgroundUsersSupported)
}
@BeforeClass
@@ -211,8 +215,13 @@
handler: Handler,
private val network: Network
) : PacketReader(handler, RCV_BUFFER_SIZE) {
+ private data class PingContext(
+ val futureReply: CompletableFuture<List<ByteArray>>,
+ val expectReplyCount: Int,
+ val replyPayloads: MutableList<ByteArray> = mutableListOf()
+ )
private var sockFd: FileDescriptor? = null
- private var futureReply: CompletableFuture<ByteArray>? = null
+ private var pingContext: PingContext? = null
override fun createFd(): FileDescriptor {
// sockFd is closed by calling super.stop()
@@ -224,6 +233,8 @@
}
override fun handlePacket(recvbuf: ByteArray, length: Int) {
+ val context = pingContext ?: return
+
// If zero-length or Type is not echo reply: ignore.
if (length == 0 || recvbuf[0] != 0x81.toByte()) {
return
@@ -231,10 +242,14 @@
// Only copy the ping data and complete the future.
val result = recvbuf.sliceArray(8..<length)
Log.i(TAG, "Received ping reply: ${result.toHexString()}")
- futureReply!!.complete(recvbuf.sliceArray(8..<length))
+ context.replyPayloads.add(recvbuf.sliceArray(8..<length))
+ if (context.replyPayloads.size == context.expectReplyCount) {
+ context.futureReply.complete(context.replyPayloads)
+ pingContext = null
+ }
}
- fun sendPing(data: ByteArray, payloadSize: Int) {
+ fun sendPing(data: ByteArray, payloadSize: Int, expectReplyCount: Int = 1) {
require(data.size == payloadSize)
// rfc4443#section-4.1: Echo Request Message
@@ -250,17 +265,20 @@
val icmp6Header = byteArrayOf(0x80.toByte(), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
val packet = icmp6Header + data
Log.i(TAG, "Sent ping: ${packet.toHexString()}")
- futureReply = CompletableFuture<ByteArray>()
+ pingContext = PingContext(
+ futureReply = CompletableFuture<List<ByteArray>>(),
+ expectReplyCount = expectReplyCount
+ )
Os.sendto(sockFd!!, packet, 0, packet.size, 0, PING_DESTINATION)
}
- fun expectPingReply(timeoutMs: Long = TIMEOUT_MS): ByteArray {
- return futureReply!!.get(timeoutMs, TimeUnit.MILLISECONDS)
+ fun expectPingReply(timeoutMs: Long = TIMEOUT_MS): List<ByteArray> {
+ return pingContext!!.futureReply.get(timeoutMs, TimeUnit.MILLISECONDS)
}
fun expectPingDropped() {
assertFailsWith(TimeoutException::class) {
- futureReply!!.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+ pingContext!!.futureReply.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
}
}
@@ -299,10 +317,23 @@
return ApfCapabilities(version, maxLen, packetFormat)
}
+ private fun isTvDeviceSupportFullNetworkingUnder2w(): Boolean {
+ return (pm.hasSystemFeature(FEATURE_LEANBACK) &&
+ pm.hasSystemFeature("com.google.android.tv.full_networking_under_2w"))
+ }
+
@Before
fun setUp() {
assume().that(pm.hasSystemFeature(FEATURE_WIFI)).isTrue()
+ // Based on GTVS-16, Android Packet Filtering (APF) is OPTIONAL for devices that fully
+ // process all network packets on CPU at all times, even in standby, while meeting
+ // the <= 2W standby power demand requirement.
+ assumeFalse(
+ "Skipping test: TV device process full networking on CPU under 2W",
+ isTvDeviceSupportFullNetworkingUnder2w()
+ )
+
networkCallback = TestableNetworkCallback()
cm.requestNetwork(
NetworkRequest.Builder()
@@ -349,10 +380,8 @@
@Test
fun testApfCapabilities() {
// APF became mandatory in Android 14 VSR.
- assume().that(getVsrApiLevel()).isAtLeast(34)
-
- // ApfFilter does not support anything but ARPHRD_ETHER.
- assertThat(caps.apfPacketFormat).isEqualTo(OsConstants.ARPHRD_ETHER)
+ val vsrApiLevel = getVsrApiLevel()
+ assume().that(vsrApiLevel).isAtLeast(34)
// DEVICEs launching with Android 14 with CHIPSETs that set ro.board.first_api_level to 34:
// - [GMS-VSR-5.3.12-003] MUST return 4 or higher as the APF version number from calls to
@@ -372,9 +401,22 @@
// ro.board.first_api_level or ro.board.api_level to 202404 or higher:
// - [GMS-VSR-5.3.12-009] MUST indicate at least 2048 bytes of usable memory from calls to
// the getApfPacketFilterCapabilities HAL method.
- if (getVsrApiLevel() >= 202404) {
+ if (vsrApiLevel >= 202404) {
assertThat(caps.maximumApfProgramSize).isAtLeast(2048)
}
+
+ // CHIPSETs (or DEVICES with CHIPSETs) that set ro.board.first_api_level or
+ // ro.board.api_level to 202504 or higher:
+ // - [VSR-5.3.12-018] MUST implement version 6 of the Android Packet Filtering (APF)
+ // interpreter in the Wi-Fi firmware.
+ // - [VSR-5.3.12-019] MUST provide at least 4000 bytes of APF RAM.
+ if (vsrApiLevel >= 202504) {
+ assertThat(caps.apfVersionSupported).isEqualTo(6000)
+ assertThat(caps.maximumApfProgramSize).isAtLeast(4000)
+ }
+
+ // ApfFilter does not support anything but ARPHRD_ETHER.
+ assertThat(caps.apfPacketFormat).isEqualTo(OsConstants.ARPHRD_ETHER)
}
// APF is backwards compatible, i.e. a v6 interpreter supports both v2 and v4 functionality.
@@ -382,6 +424,10 @@
assume().that(caps.apfVersionSupported).isAtLeast(version)
}
+ fun assumeNotCuttlefish() {
+ assume().that(SystemProperties.get("ro.product.board", "")).isNotEqualTo("cutf")
+ }
+
fun installProgram(bytes: ByteArray) {
val prog = bytes.toHexString()
val result = runShellCommandOrThrow("cmd network_stack apf $ifname install $prog").trim()
@@ -465,6 +511,7 @@
// should be turned on.
assume().that(getVsrApiLevel()).isAtLeast(34)
assumeApfVersionSupportAtLeast(4)
+ assumeNotCuttlefish()
// clear any active APF filter
clearApfMemory()
@@ -478,7 +525,7 @@
}
val data = ByteArray(payloadSize).also { Random.nextBytes(it) }
packetReader.sendPing(data, payloadSize)
- assertThat(packetReader.expectPingReply()).isEqualTo(data)
+ assertThat(packetReader.expectPingReply()[0]).isEqualTo(data)
// Generate an APF program that drops the next ping
val gen = ApfV4Generator(
@@ -517,6 +564,7 @@
assume().that(getVsrApiLevel()).isAtLeast(34)
// Test v4 memory slots on both v4 and v6 interpreters.
assumeApfVersionSupportAtLeast(4)
+ assumeNotCuttlefish()
clearApfMemory()
val gen = ApfV4Generator(
caps.apfVersionSupported,
@@ -575,6 +623,7 @@
// should be turned on.
assume().that(getVsrApiLevel()).isAtLeast(34)
assumeApfVersionSupportAtLeast(4)
+ assumeNotCuttlefish()
clearApfMemory()
val gen = ApfV4Generator(
caps.apfVersionSupported,
@@ -617,6 +666,7 @@
@Test
fun testFilterAge16384thsIncreasesBetweenPackets() {
assumeApfVersionSupportAtLeast(6000)
+ assumeNotCuttlefish()
clearApfMemory()
val gen = ApfV6Generator(
caps.apfVersionSupported,
@@ -666,6 +716,7 @@
@Test
fun testReplyPing() {
assumeApfVersionSupportAtLeast(6000)
+ assumeNotCuttlefish()
installProgram(ByteArray(caps.maximumApfProgramSize) { 0 }) // Clear previous program
readProgram() // Ensure installation is complete
@@ -690,69 +741,80 @@
// increase PASSED_IPV6_ICMP counter
// pass
// else
- // transmit a ICMPv6 echo request packet with the first byte of the payload in the reply
- // increase DROPPED_IPV6_MULTICAST_PING counter
+ // transmit 3 ICMPv6 echo requests with random first byte
+ // increase DROPPED_IPV6_NS_REPLIED_NON_DAD counter
// drop
- val program = gen
- .addLoad16(R0, ETH_ETHERTYPE_OFFSET)
+ gen.addLoad16(R0, ETH_ETHERTYPE_OFFSET)
.addJumpIfR0NotEquals(ETH_P_IPV6.toLong(), skipPacketLabel)
.addLoad8(R0, IPV6_NEXT_HEADER_OFFSET)
.addJumpIfR0NotEquals(IPPROTO_ICMPV6.toLong(), skipPacketLabel)
.addLoad8(R0, ICMP6_TYPE_OFFSET)
- .addJumpIfR0NotEquals(0x81, skipPacketLabel) // Echo reply type
+ .addJumpIfR0NotEquals(ICMP6_ECHO_REPLY.toLong(), skipPacketLabel)
.addLoadFromMemory(R0, MemorySlot.PACKET_SIZE)
.addCountAndPassIfR0Equals(
- (ETHER_HEADER_LEN + IPV6_HEADER_LEN + PING_HEADER_LENGTH + firstByte.size)
- .toLong(),
- PASSED_IPV6_ICMP
+ (ETHER_HEADER_LEN + IPV6_HEADER_LEN + PING_HEADER_LENGTH + firstByte.size)
+ .toLong(),
+ PASSED_IPV6_ICMP
)
- // Ping Packet Generation
- .addAllocate(pingRequestPktLen)
- // Eth header
- .addPacketCopy(ETHER_SRC_ADDR_OFFSET, ETHER_ADDR_LEN) // dst MAC address
- .addPacketCopy(ETHER_DST_ADDR_OFFSET, ETHER_ADDR_LEN) // src MAC address
- .addWriteU16(ETH_P_IPV6) // IPv6 type
- // IPv6 Header
- .addWrite32(0x60000000) // IPv6 Header: version, traffic class, flowlabel
- // payload length (2 bytes) | next header: ICMPv6 (1 byte) | hop limit (1 byte)
- .addWrite32(pingRequestIpv6PayloadLen shl 16 or (IPPROTO_ICMPV6 shl 8 or 64))
- .addPacketCopy(IPV6_DEST_ADDR_OFFSET, IPV6_ADDR_LEN) // src ip
- .addPacketCopy(IPV6_SRC_ADDR_OFFSET, IPV6_ADDR_LEN) // dst ip
- // ICMPv6
- .addWriteU8(0x80) // type: echo request
- .addWriteU8(0) // code
- .addWriteU16(pingRequestIpv6PayloadLen) // checksum
- // identifier
- .addPacketCopy(ETHER_HEADER_LEN + IPV6_HEADER_LEN + ICMPV6_HEADER_MIN_LEN, 2)
- .addWriteU16(0) // sequence number
- .addDataCopy(firstByte) // data
- .addTransmitL4(
+
+ val numOfPacketToTransmit = 3
+ val expectReplyPayloads = (0 until numOfPacketToTransmit).map { Random.nextBytes(1) }
+ expectReplyPayloads.forEach { replyPingPayload ->
+ // Ping Packet Generation
+ gen.addAllocate(pingRequestPktLen)
+ // Eth header
+ .addPacketCopy(ETHER_SRC_ADDR_OFFSET, ETHER_ADDR_LEN) // dst MAC address
+ .addPacketCopy(ETHER_DST_ADDR_OFFSET, ETHER_ADDR_LEN) // src MAC address
+ .addWriteU16(ETH_P_IPV6) // IPv6 type
+ // IPv6 Header
+ .addWrite32(0x60000000) // IPv6 Header: version, traffic class, flowlabel
+ // payload length (2 bytes) | next header: ICMPv6 (1 byte) | hop limit (1 byte)
+ .addWrite32(pingRequestIpv6PayloadLen shl 16 or (IPPROTO_ICMPV6 shl 8 or 64))
+ .addPacketCopy(IPV6_DEST_ADDR_OFFSET, IPV6_ADDR_LEN) // src ip
+ .addPacketCopy(IPV6_SRC_ADDR_OFFSET, IPV6_ADDR_LEN) // dst ip
+ // ICMPv6
+ .addWriteU8(ICMP6_ECHO_REQUEST)
+ .addWriteU8(0) // code
+ .addWriteU16(pingRequestIpv6PayloadLen) // checksum
+ // identifier
+ .addPacketCopy(ETHER_HEADER_LEN + IPV6_HEADER_LEN + ICMPV6_HEADER_MIN_LEN, 2)
+ .addWriteU16(0) // sequence number
+ .addDataCopy(replyPingPayload) // data
+ .addTransmitL4(
ETHER_HEADER_LEN, // ip_ofs
ICMP6_CHECKSUM_OFFSET, // csum_ofs
IPV6_SRC_ADDR_OFFSET, // csum_start
IPPROTO_ICMPV6, // partial_sum
false // udp
- )
- // Warning: the program abuse DROPPED_IPV6_MULTICAST_PING for debugging purpose
- .addCountAndDrop(DROPPED_IPV6_MULTICAST_PING)
- .defineLabel(skipPacketLabel)
- .addPass()
- .generate()
+ )
+ }
+ // Warning: the program abuse DROPPED_IPV6_NS_REPLIED_NON_DAD for debugging purpose
+ gen.addCountAndDrop(DROPPED_IPV6_NS_REPLIED_NON_DAD)
+ .defineLabel(skipPacketLabel)
+ .addPass()
+
+ val program = gen.generate()
installAndVerifyProgram(program)
- packetReader.sendPing(payload, payloadSize)
-
- val replyPayload = try {
+ packetReader.sendPing(payload, payloadSize, expectReplyCount = numOfPacketToTransmit)
+ val replyPayloads = try {
packetReader.expectPingReply(TIMEOUT_MS * 2)
} catch (e: TimeoutException) {
- byteArrayOf() // Empty payload if timeout occurs
+ emptyList()
}
val apfCounterTracker = ApfCounterTracker()
apfCounterTracker.updateCountersFromData(readProgram())
Log.i(TAG, "counter map: ${apfCounterTracker.counters}")
- assertThat(replyPayload).isEqualTo(firstByte)
+ assertThat(replyPayloads.size).isEqualTo(expectReplyPayloads.size)
+
+ // Sort the payload list before comparison to ensure consistency.
+ val sortedReplyPayloads = replyPayloads.sortedBy { it[0] }
+ val sortedExpectReplyPayloads = expectReplyPayloads.sortedBy { it[0] }
+ for (i in sortedReplyPayloads.indices) {
+ assertThat(sortedReplyPayloads[i]).isEqualTo(sortedExpectReplyPayloads[i])
+ }
}
}
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
index ceb48d4..faaadee 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
@@ -88,9 +88,11 @@
import com.android.net.module.util.ArrayTrackRecord;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.com.android.testutils.CarrierConfigRule;
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -110,6 +112,9 @@
public class ConnectivityDiagnosticsManagerTest {
private static final String TAG = ConnectivityDiagnosticsManagerTest.class.getSimpleName();
+ @Rule
+ public final CarrierConfigRule mCarrierConfigRule = new CarrierConfigRule();
+
private static final int CALLBACK_TIMEOUT_MILLIS = 5000;
private static final int NO_CALLBACK_INVOKED_TIMEOUT = 500;
private static final long TIMESTAMP = 123456789L;
@@ -264,9 +269,6 @@
doBroadcastCarrierConfigsAndVerifyOnConnectivityReportAvailable(
subId, carrierConfigReceiver, testNetworkCallback);
}, () -> {
- runWithShellPermissionIdentity(
- () -> mCarrierConfigManager.overrideConfig(subId, null),
- android.Manifest.permission.MODIFY_PHONE_STATE);
mConnectivityManager.unregisterNetworkCallback(testNetworkCallback);
mContext.unregisterReceiver(carrierConfigReceiver);
});
@@ -291,9 +293,9 @@
CarrierConfigManager.KEY_CARRIER_CERTIFICATE_STRING_ARRAY,
new String[] {getCertHashForThisPackage()});
+ mCarrierConfigRule.addConfigOverrides(subId, carrierConfigs);
runWithShellPermissionIdentity(
() -> {
- mCarrierConfigManager.overrideConfig(subId, carrierConfigs);
mCarrierConfigManager.notifyConfigChangedForSubId(subId);
},
android.Manifest.permission.MODIFY_PHONE_STATE);
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 9457a42..00c87a3 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -127,6 +127,7 @@
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
import android.annotation.NonNull;
@@ -922,6 +923,7 @@
public void testOpenConnection() throws Exception {
assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
assumeTrue(mPackageManager.hasSystemFeature(FEATURE_TELEPHONY));
+ assumeFalse(Build.MODEL.contains("Cuttlefish"));
Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();
Network cellNetwork = networkCallbackRule.requestCell();
@@ -2329,8 +2331,10 @@
// Verify that turning airplane mode off takes effect as expected.
// connectToCell only registers a request, it cannot / does not need to be called twice
- mCtsNetUtils.ensureWifiConnected();
- if (verifyWifi) waitForAvailable(wifiCb);
+ if (verifyWifi) {
+ mCtsNetUtils.ensureWifiConnected();
+ waitForAvailable(wifiCb);
+ }
if (supportTelephony) {
telephonyCb.eventuallyExpect(
CallbackEntry.AVAILABLE, CELL_DATA_AVAILABLE_TIMEOUT_MS);
diff --git a/tests/cts/net/src/android/net/cts/DnsResolverTapTest.kt b/tests/cts/net/src/android/net/cts/DnsResolverTapTest.kt
new file mode 100644
index 0000000..ff608f2
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/DnsResolverTapTest.kt
@@ -0,0 +1,185 @@
+/*
+ * 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 android.net.cts
+
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.Manifest.permission.READ_DEVICE_CONFIG
+import android.net.DnsResolver
+import android.net.InetAddresses.parseNumericAddress
+import android.net.IpPrefix
+import android.net.MacAddress
+import android.net.RouteInfo
+import android.os.CancellationSignal
+import android.os.HandlerThread
+import android.os.SystemClock
+import android.platform.test.annotations.AppModeFull
+import android.provider.DeviceConfig
+import android.provider.DeviceConfig.NAMESPACE_NETD_NATIVE
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN
+import com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN
+import com.android.net.module.util.NetworkStackConstants.UDP_HEADER_LEN
+import com.android.testutils.AutoReleaseNetworkCallbackRule
+import com.android.testutils.DeviceConfigRule
+import com.android.testutils.DnsResolverModuleTest
+import com.android.testutils.IPv6UdpFilter
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.RouterAdvertisementResponder
+import com.android.testutils.TapPacketReaderRule
+import com.android.testutils.TestableNetworkAgent
+import com.android.testutils.TestDnsPacket
+import com.android.testutils.com.android.testutils.SetFeatureFlagsRule
+import com.android.testutils.runAsShell
+import java.net.Inet6Address
+import java.net.InetAddress
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private val TEST_DNSSERVER_MAC = MacAddress.fromString("00:11:22:33:44:55")
+private val TAG = DnsResolverTapTest::class.java.simpleName
+private const val TEST_TIMEOUT_MS = 10_000L
+
+@AppModeFull(reason = "Test networks cannot be created in instant app mode")
+@DnsResolverModuleTest
+@RunWith(AndroidJUnit4::class)
+class DnsResolverTapTest {
+ private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
+ private val handlerThread = HandlerThread(TAG)
+
+ @get:Rule(order = 1)
+ val deviceConfigRule = DeviceConfigRule()
+
+ @get:Rule(order = 2)
+ val featureFlagsRule = SetFeatureFlagsRule(
+ setFlagsMethod = { name, enabled ->
+ val value = when (enabled) {
+ null -> null
+ true -> "1"
+ false -> "0"
+ }
+ deviceConfigRule.setConfig(NAMESPACE_NETD_NATIVE, name, value)
+ },
+ getFlagsMethod = {
+ runAsShell(READ_DEVICE_CONFIG) {
+ DeviceConfig.getInt(NAMESPACE_NETD_NATIVE, it, 0) == 1
+ }
+ }
+ )
+
+ @get:Rule(order = 3)
+ val packetReaderRule = TapPacketReaderRule()
+
+ @get:Rule(order = 4)
+ val cbRule = AutoReleaseNetworkCallbackRule()
+
+ private val ndResponder by lazy { RouterAdvertisementResponder(packetReaderRule.reader) }
+ private val dnsServerAddr by lazy {
+ parseNumericAddress("fe80::124%${packetReaderRule.iface.interfaceName}") as Inet6Address
+ }
+ private lateinit var agent: TestableNetworkAgent
+
+ @Before
+ fun setUp() {
+ handlerThread.start()
+ val interfaceName = packetReaderRule.iface.interfaceName
+ val cb = cbRule.requestNetwork(TestableNetworkAgent.makeNetworkRequestForInterface(
+ interfaceName))
+ agent = runAsShell(MANAGE_TEST_NETWORKS) {
+ TestableNetworkAgent.createOnInterface(context, handlerThread.looper,
+ interfaceName, TEST_TIMEOUT_MS)
+ }
+ ndResponder.addNeighborEntry(TEST_DNSSERVER_MAC, dnsServerAddr)
+ ndResponder.start()
+ agent.lp.apply {
+ addDnsServer(dnsServerAddr)
+ // A default route is needed for DnsResolver.java to send queries over IPv6
+ // (see usage of DnsUtils.haveIpv6).
+ addRoute(RouteInfo(IpPrefix("::/0"), null, null))
+ }
+ agent.sendLinkProperties(agent.lp)
+ cb.eventuallyExpect<LinkPropertiesChanged> { it.lp.dnsServers.isNotEmpty() }
+ }
+
+ @After
+ fun tearDown() {
+ ndResponder.stop()
+ if (::agent.isInitialized) {
+ agent.unregister()
+ }
+ handlerThread.quitSafely()
+ handlerThread.join()
+ }
+
+ private class DnsCallback : DnsResolver.Callback<List<InetAddress>> {
+ override fun onAnswer(answer: List<InetAddress>, rcode: Int) = Unit
+ override fun onError(error: DnsResolver.DnsException) = Unit
+ }
+
+ /**
+ * Run a cancellation test.
+ *
+ * @param domain Domain name to query
+ * @param waitTimeForNoRetryAfterCancellationMs If positive, cancel the query and wait for that
+ * delay to check no retry is sent.
+ * @return The duration it took to receive all expected replies.
+ */
+ fun doCancellationTest(domain: String, waitTimeForNoRetryAfterCancellationMs: Long): Long {
+ val cancellationSignal = CancellationSignal()
+ val dnsCb = DnsCallback()
+ val queryStart = SystemClock.elapsedRealtime()
+ DnsResolver.getInstance().query(
+ agent.network, domain, 0 /* flags */,
+ Runnable::run /* executor */, cancellationSignal, dnsCb
+ )
+
+ if (waitTimeForNoRetryAfterCancellationMs > 0) {
+ cancellationSignal.cancel()
+ }
+ // Filter for queries on UDP port 53 for the specified domain
+ val filter = IPv6UdpFilter(dstPort = 53).and {
+ TestDnsPacket(
+ it.copyOfRange(ETHER_HEADER_LEN + IPV6_HEADER_LEN + UDP_HEADER_LEN, it.size),
+ dstAddr = dnsServerAddr
+ ).isQueryFor(domain, DnsResolver.TYPE_AAAA)
+ }
+
+ val reader = packetReaderRule.reader
+ assertNotNull(reader.poll(TEST_TIMEOUT_MS, filter), "Original query not found")
+ if (waitTimeForNoRetryAfterCancellationMs > 0) {
+ assertNull(reader.poll(waitTimeForNoRetryAfterCancellationMs, filter),
+ "Expected no retry query")
+ } else {
+ assertNotNull(reader.poll(TEST_TIMEOUT_MS, filter), "Retry query not found")
+ }
+ return SystemClock.elapsedRealtime() - queryStart
+ }
+
+ @SetFeatureFlagsRule.FeatureFlag("no_retry_after_cancel", true)
+ @Test
+ fun testCancellation() {
+ val timeWithRetryWhenNotCancelled = doCancellationTest("test1.example.com",
+ waitTimeForNoRetryAfterCancellationMs = 0L)
+ doCancellationTest("test2.example.com",
+ waitTimeForNoRetryAfterCancellationMs = timeWithRetryWhenNotCancelled + 50L)
+ }
+}
\ No newline at end of file
diff --git a/tests/cts/net/src/android/net/cts/DnsResolverTest.java b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
index fa44ae9..b66b853 100644
--- a/tests/cts/net/src/android/net/cts/DnsResolverTest.java
+++ b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
@@ -58,6 +58,7 @@
import androidx.test.runner.AndroidJUnit4;
import com.android.net.module.util.DnsPacket;
+import com.android.testutils.ConnectivityDiagnosticsCollector;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.DeviceConfigRule;
@@ -394,7 +395,22 @@
@Test
@DnsResolverModuleTest
public void testRawQueryNXDomainWithPrivateDns() throws Exception {
- doTestRawQueryNXDomainWithPrivateDns(mExecutor);
+ try {
+ doTestRawQueryNXDomainWithPrivateDns(mExecutor);
+ } catch (Throwable e) {
+ final ConnectivityDiagnosticsCollector collector =
+ ConnectivityDiagnosticsCollector.getInstance();
+ if (collector != null) {
+ // IWLAN on U QPR3 release may cause failures in this test, see
+ // CarrierConfigSetupTest which is supposed to avoid the issue. Collect IWLAN
+ // related dumpsys if the test still fails.
+ collector.collectDumpsys("carrier_config", e);
+ collector.collectDumpsys("telecom", e);
+ collector.collectDumpsys("telephony_ims", e);
+ collector.collectDumpsys("telephony.registry", e);
+ }
+ throw e;
+ }
}
@Test
diff --git a/tests/cts/net/src/android/net/cts/DnsTest.java b/tests/cts/net/src/android/net/cts/DnsTest.java
index b1e5680..e367b7d 100644
--- a/tests/cts/net/src/android/net/cts/DnsTest.java
+++ b/tests/cts/net/src/android/net/cts/DnsTest.java
@@ -18,6 +18,7 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeFalse;
import android.content.Context;
import android.content.pm.PackageManager;
@@ -26,6 +27,7 @@
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkInfo;
+import android.os.Build;
import android.os.SystemClock;
import android.util.Log;
@@ -299,6 +301,8 @@
}
private void ensureIpv6Connectivity() throws InterruptedException {
+ assumeFalse(Build.MODEL.contains("Cuttlefish"));
+
CountDownLatch latch = new CountDownLatch(1);
final int TIMEOUT_MS = 5_000;
diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
index 5b2c9f7..06f2075 100644
--- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
@@ -605,6 +605,9 @@
}
private fun assumeNoInterfaceForTetheringAvailable() {
+ // Requesting a tethered interface will stop IpClient. Prevent it from doing so
+ // if adb is connected over ethernet.
+ assumeFalse(isAdbOverEthernet())
// Interfaces that have configured NetworkCapabilities will never be used for tethering,
// see aosp/2123900.
try {
@@ -1069,6 +1072,9 @@
@Test
fun testSetTetheringInterfaceMode_disableEnableEthernet() {
+ // do not run this test if an interface that can be used for tethering already exists.
+ assumeNoInterfaceForTetheringAvailable()
+
val listener = EthernetStateListener()
addInterfaceStateListener(listener)
diff --git a/tests/cts/net/src/android/net/cts/L2capNetworkSpecifierTest.kt b/tests/cts/net/src/android/net/cts/L2capNetworkSpecifierTest.kt
new file mode 100644
index 0000000..484cce8
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/L2capNetworkSpecifierTest.kt
@@ -0,0 +1,117 @@
+/*
+ * 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 android.net.cts
+
+import android.net.L2capNetworkSpecifier
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_6LOWPAN
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_ANY
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_NONE
+import android.net.L2capNetworkSpecifier.PSM_ANY
+import android.net.L2capNetworkSpecifier.ROLE_CLIENT
+import android.net.L2capNetworkSpecifier.ROLE_SERVER
+import android.net.MacAddress
+import android.os.Build
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.assertParcelingIsLossless
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@ConnectivityModuleTest
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class L2capNetworkSpecifierTest {
+ @Test
+ fun testParcelUnparcel() {
+ val remoteMac = MacAddress.fromString("01:02:03:04:05:06")
+ val specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_CLIENT)
+ .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+ .setPsm(42)
+ .setRemoteAddress(remoteMac)
+ .build()
+ assertParcelingIsLossless(specifier)
+ }
+
+ @Test
+ fun testGetters() {
+ val remoteMac = MacAddress.fromString("11:22:33:44:55:66")
+ val specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_CLIENT)
+ .setHeaderCompression(HEADER_COMPRESSION_NONE)
+ .setPsm(123)
+ .setRemoteAddress(remoteMac)
+ .build()
+ assertEquals(ROLE_CLIENT, specifier.getRole())
+ assertEquals(HEADER_COMPRESSION_NONE, specifier.getHeaderCompression())
+ assertEquals(123, specifier.getPsm())
+ assertEquals(remoteMac, specifier.getRemoteAddress())
+ }
+
+ @Test
+ fun testCanBeSatisfiedBy() {
+ val blanketOffer = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(HEADER_COMPRESSION_ANY)
+ .setPsm(PSM_ANY)
+ .build()
+
+ val reservedOffer = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+ .setPsm(42)
+ .build()
+
+ val clientOffer = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_CLIENT)
+ .setHeaderCompression(HEADER_COMPRESSION_ANY)
+ .build()
+
+ val serverReservation = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+ .build()
+
+ assertTrue(serverReservation.canBeSatisfiedBy(blanketOffer))
+ assertTrue(serverReservation.canBeSatisfiedBy(reservedOffer))
+ // Note: serverReservation can be filed using reserveNetwork, or it could be a regular
+ // request filed using requestNetwork.
+ assertFalse(serverReservation.canBeSatisfiedBy(clientOffer))
+
+ val clientRequest = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_CLIENT)
+ .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+ .setRemoteAddress(MacAddress.fromString("00:01:02:03:04:05"))
+ .setPsm(42)
+ .build()
+
+ assertTrue(clientRequest.canBeSatisfiedBy(clientOffer))
+ // Note: the BlanketOffer also includes a RES_ID_MATCH_ALL_RESERVATIONS. Since the
+ // clientRequest is not a reservation, it won't match that request to begin with.
+ assertFalse(clientRequest.canBeSatisfiedBy(blanketOffer))
+ assertFalse(clientRequest.canBeSatisfiedBy(reservedOffer))
+
+ val matchAny = L2capNetworkSpecifier.Builder().build()
+ assertTrue(matchAny.canBeSatisfiedBy(blanketOffer))
+ assertTrue(matchAny.canBeSatisfiedBy(reservedOffer))
+ assertTrue(matchAny.canBeSatisfiedBy(clientOffer))
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index 815c3a5..8fcc703 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -117,13 +117,13 @@
import com.android.testutils.ConnectivityModuleTest
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.PollPacketReader
import com.android.testutils.RecorderCallback.CallbackEntry.Available
import com.android.testutils.RecorderCallback.CallbackEntry.BlockedStatus
import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
import com.android.testutils.RecorderCallback.CallbackEntry.Losing
import com.android.testutils.RecorderCallback.CallbackEntry.Lost
-import com.android.testutils.PollPacketReader
import com.android.testutils.TestableNetworkAgent
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnAddKeepalivePacketFilter
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnAutomaticReconnectDisabled
@@ -140,6 +140,7 @@
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnValidationStatus
import com.android.testutils.TestableNetworkCallback
import com.android.testutils.assertThrows
+import com.android.testutils.com.android.testutils.CarrierConfigRule
import com.android.testutils.runAsShell
import com.android.testutils.tryTest
import com.android.testutils.waitForIdle
@@ -149,8 +150,8 @@
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.Socket
-import java.security.MessageDigest
import java.nio.ByteBuffer
+import java.security.MessageDigest
import java.time.Duration
import java.util.Arrays
import java.util.Random
@@ -167,6 +168,7 @@
import org.junit.After
import org.junit.Assume.assumeTrue
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
@@ -178,10 +180,12 @@
import org.mockito.Mockito.verify
private const val TAG = "NetworkAgentTest"
+
// This test doesn't really have a constraint on how fast the methods should return. If it's
// going to fail, it will simply wait forever, so setting a high timeout lowers the flake ratio
// without affecting the run time of successful runs. Thus, set a very high timeout.
private const val DEFAULT_TIMEOUT_MS = 5000L
+
// When waiting for a NetworkCallback to determine there was no timeout, waiting is the
// only possible thing (the relevant handler is the one in the real ConnectivityService,
// and then there is the Binder call), so have a short timeout for this as it will be
@@ -223,6 +227,9 @@
@IgnoreUpTo(Build.VERSION_CODES.R)
@RunWith(DevSdkIgnoreRunner::class)
class NetworkAgentTest {
+ @get:Rule
+ val carrierConfigRule = CarrierConfigRule()
+
private val LOCAL_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1")
private val REMOTE_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.2")
@@ -378,8 +385,12 @@
// Ensure this NetworkAgent is never unneeded by filing a request with its specifier.
requestNetwork(makeTestNetworkRequest(specifier), callback)
val nc = makeTestNetworkCapabilities(specifier, transports)
- val agent = createNetworkAgent(context, initialConfig = initialConfig, initialLp = lp,
- initialNc = nc)
+ val agent = createNetworkAgent(
+ context,
+ initialConfig = initialConfig,
+ initialLp = lp,
+ initialNc = nc
+ )
agent.setTeardownDelayMillis(0)
// Connect the agent and verify initial status callbacks.
agent.register()
@@ -414,7 +425,8 @@
private fun createTunInterface(addrs: Collection<LinkAddress> = emptyList()):
TestNetworkInterface = realContext.getSystemService(
- TestNetworkManager::class.java)!!.createTunInterface(addrs).also {
+ TestNetworkManager::class.java
+ )!!.createTunInterface(addrs).also {
ifacesToCleanUp.add(it)
}
@@ -546,9 +558,12 @@
@Test
fun testSocketKeepalive(): Unit = createNetworkAgentWithFakeCS().let { agent ->
val packet = NattKeepalivePacketData(
- LOCAL_IPV4_ADDRESS /* srcAddress */, 1234 /* srcPort */,
- REMOTE_IPV4_ADDRESS /* dstAddress */, 4567 /* dstPort */,
- ByteArray(100 /* size */))
+ LOCAL_IPV4_ADDRESS /* srcAddress */,
+ 1234 /* srcPort */,
+ REMOTE_IPV4_ADDRESS /* dstAddress */,
+ 4567 /* dstPort */,
+ ByteArray(100 /* size */)
+ )
val slot = 4
val interval = 37
@@ -653,8 +668,13 @@
uid: Int,
expectUidsPresent: Boolean
) {
- doTestAllowedUids(intArrayOf(transport), uid, expectUidsPresent,
- specifier = null, transportInfo = null)
+ doTestAllowedUids(
+ intArrayOf(transport),
+ uid,
+ expectUidsPresent,
+ specifier = null,
+ transportInfo = null
+ )
}
private fun doTestAllowedUidsWithSubId(
@@ -689,21 +709,28 @@
private fun setHoldCarrierPrivilege(hold: Boolean, subId: Int) {
fun getCertHash(): String {
- val pkgInfo = realContext.packageManager.getPackageInfo(realContext.opPackageName,
- PackageManager.GET_SIGNATURES)
+ val pkgInfo = realContext.packageManager.getPackageInfo(
+ realContext.opPackageName,
+ PackageManager.GET_SIGNATURES
+ )
val digest = MessageDigest.getInstance("SHA-256")
val certHash = digest.digest(pkgInfo.signatures!![0]!!.toByteArray())
return UiccUtil.bytesToHexString(certHash)!!
}
val tm = realContext.getSystemService(TelephonyManager::class.java)!!
- val ccm = realContext.getSystemService(CarrierConfigManager::class.java)!!
val cv = ConditionVariable()
val cpb = PrivilegeWaiterCallback(cv)
- tryTest {
+ // The lambda below is capturing |cpb|, whose type inherits from a class that appeared in
+ // T. This means the lambda will compile as a private method of this class taking a
+ // PrivilegeWaiterCallback argument. As JUnit uses reflection to enumerate all methods
+ // including private methods, this would fail with a link error when running on S-.
+ // To solve this, make the lambda serializable, which causes the compiler to emit a
+ // synthetic class instead of a synthetic method.
+ tryTest @JvmSerializableLambda {
val slotIndex = SubscriptionManager.getSlotIndex(subId)!!
- runAsShell(READ_PRIVILEGED_PHONE_STATE) {
+ runAsShell(READ_PRIVILEGED_PHONE_STATE) @JvmSerializableLambda {
tm.registerCarrierPrivilegesCallback(slotIndex, { it.run() }, cpb)
}
// Wait for the callback to be registered
@@ -716,21 +743,16 @@
}
return@tryTest
}
- cv.close()
- runAsShell(MODIFY_PHONE_STATE) {
- val carrierConfigs = if (hold) {
- PersistableBundle().also {
- it.putStringArray(CarrierConfigManager.KEY_CARRIER_CERTIFICATE_STRING_ARRAY,
- arrayOf(getCertHash()))
- }
- } else {
- null
- }
- ccm.overrideConfig(subId, carrierConfigs)
+ if (hold) {
+ carrierConfigRule.addConfigOverrides(subId, PersistableBundle().also {
+ it.putStringArray(CarrierConfigManager.KEY_CARRIER_CERTIFICATE_STRING_ARRAY,
+ arrayOf(getCertHash()))
+ })
+ } else {
+ carrierConfigRule.cleanUpNow()
}
- assertTrue(cv.block(DEFAULT_TIMEOUT_MS), "Can't change carrier privilege")
- } cleanup {
- runAsShell(READ_PRIVILEGED_PHONE_STATE) {
+ } cleanup @JvmSerializableLambda {
+ runAsShell(READ_PRIVILEGED_PHONE_STATE) @JvmSerializableLambda {
tm.unregisterCarrierPrivilegesCallback(cpb)
}
}
@@ -744,9 +766,15 @@
val cv = ConditionVariable()
val cpb = CarrierServiceChangedWaiterCallback(cv)
- tryTest {
+ // The lambda below is capturing |cpb|, whose type inherits from a class that appeared in
+ // T. This means the lambda will compile as a private method of this class taking a
+ // PrivilegeWaiterCallback argument. As JUnit uses reflection to enumerate all methods
+ // including private methods, this would fail with a link error when running on S-.
+ // To solve this, make the lambda serializable, which causes the compiler to emit a
+ // synthetic class instead of a synthetic method.
+ tryTest @JvmSerializableLambda {
val slotIndex = SubscriptionManager.getSlotIndex(subId)!!
- runAsShell(READ_PRIVILEGED_PHONE_STATE) {
+ runAsShell(READ_PRIVILEGED_PHONE_STATE) @JvmSerializableLambda {
tm.registerCarrierPrivilegesCallback(slotIndex, { it.run() }, cpb)
}
// Wait for the callback to be registered
@@ -768,8 +796,8 @@
}
}
assertTrue(cv.block(DEFAULT_TIMEOUT_MS), "Can't modify carrier service package")
- } cleanup {
- runAsShell(READ_PRIVILEGED_PHONE_STATE) {
+ } cleanup @JvmSerializableLambda {
+ runAsShell(READ_PRIVILEGED_PHONE_STATE) @JvmSerializableLambda {
tm.unregisterCarrierPrivilegesCallback(cpb)
}
}
@@ -799,14 +827,19 @@
val uid = try {
realContext.packageManager.getApplicationInfo(servicePackage, 0).uid
} catch (e: PackageManager.NameNotFoundException) {
- fail("$servicePackage could not be installed, please check the SuiteApkInstaller" +
- " installed CtsCarrierServicePackage.apk", e)
+ fail(
+ "$servicePackage could not be installed, please check the SuiteApkInstaller" +
+ " installed CtsCarrierServicePackage.apk",
+ e
+ )
}
val tm = realContext.getSystemService(TelephonyManager::class.java)!!
val defaultSubId = SubscriptionManager.getDefaultSubscriptionId()
- assertTrue(defaultSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID,
- "getDefaultSubscriptionId returns INVALID_SUBSCRIPTION_ID")
+ assertTrue(
+ defaultSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID,
+ "getDefaultSubscriptionId returns INVALID_SUBSCRIPTION_ID"
+ )
tryTest {
// This process is not the carrier service UID, so allowedUids should be ignored in all
// the following cases.
@@ -910,8 +943,10 @@
// If using the int ranking, agent1 must be upgraded to a better score so that there is
// no ambiguity when agent2 connects that agent1 is still better. If using policy
// ranking, this is not necessary.
- agent1.sendNetworkScore(NetworkScore.Builder().setLegacyInt(BETTER_NETWORK_SCORE)
- .build())
+ agent1.sendNetworkScore(
+ NetworkScore.Builder().setLegacyInt(BETTER_NETWORK_SCORE)
+ .build()
+ )
// Connect the second agent.
val (agent2, _) = createConnectedNetworkAgent()
@@ -920,10 +955,12 @@
// virtue of already satisfying the request.
callback.assertNoCallback(NO_CALLBACK_TIMEOUT)
// Now downgrade the score and expect the callback now prefers agent2
- agent1.sendNetworkScore(NetworkScore.Builder()
+ agent1.sendNetworkScore(
+ NetworkScore.Builder()
.setLegacyInt(WORSE_NETWORK_SCORE)
.setExiting(true)
- .build())
+ .build()
+ )
callback.expect<Available>(agent2.network!!)
// tearDown() will unregister the requests and agents
@@ -968,16 +1005,20 @@
// Check that the default network's transport is propagated to the VPN.
var vpnNc = mCM.getNetworkCapabilities(agent.network!!)
assertNotNull(vpnNc)
- assertEquals(VpnManager.TYPE_VPN_SERVICE,
- (vpnNc.transportInfo as VpnTransportInfo).type)
+ assertEquals(
+ VpnManager.TYPE_VPN_SERVICE,
+ (vpnNc.transportInfo as VpnTransportInfo).type
+ )
assertEquals(mySessionId, (vpnNc.transportInfo as VpnTransportInfo).sessionId)
val testAndVpn = intArrayOf(TRANSPORT_TEST, TRANSPORT_VPN)
assertTrue(vpnNc.hasAllTransports(testAndVpn))
assertFalse(vpnNc.hasCapability(NET_CAPABILITY_NOT_VPN))
- assertTrue(vpnNc.hasAllTransports(defaultNetworkTransports),
- "VPN transports ${Arrays.toString(vpnNc.transportTypes)}" +
- " lacking transports from ${Arrays.toString(defaultNetworkTransports)}")
+ assertTrue(
+ vpnNc.hasAllTransports(defaultNetworkTransports),
+ "VPN transports ${Arrays.toString(vpnNc.transportTypes)}" +
+ " lacking transports from ${Arrays.toString(defaultNetworkTransports)}"
+ )
// Check that when no underlying networks are announced the underlying transport disappears.
agent.setUnderlyingNetworks(listOf<Network>())
@@ -999,9 +1040,11 @@
// underlying networks, and because not congested, not roaming, and not suspended are the
// default anyway. It's still useful as an extra check though.
vpnNc = mCM.getNetworkCapabilities(agent.network!!)!!
- for (cap in listOf(NET_CAPABILITY_NOT_CONGESTED,
- NET_CAPABILITY_NOT_ROAMING,
- NET_CAPABILITY_NOT_SUSPENDED)) {
+ for (cap in listOf(
+ NET_CAPABILITY_NOT_CONGESTED,
+ NET_CAPABILITY_NOT_ROAMING,
+ NET_CAPABILITY_NOT_SUSPENDED
+ )) {
val capStr = valueToString(NetworkCapabilities::class.java, "NET_CAPABILITY_", cap)
if (defaultNetworkCapabilities.hasCapability(cap) && !vpnNc.hasCapability(cap)) {
fail("$capStr not propagated from underlying: $defaultNetworkCapabilities")
@@ -1026,13 +1069,15 @@
doReturn(mockCm).`when`(mockContext).getSystemService(Context.CONNECTIVITY_SERVICE)
val agent = createNetworkAgent(mockContext)
agent.register()
- verify(mockCm).registerNetworkAgent(any(),
- argThat<NetworkInfo> { it.detailedState == NetworkInfo.DetailedState.CONNECTING },
- any(LinkProperties::class.java),
- any(NetworkCapabilities::class.java),
- any(NetworkScore::class.java),
- any(NetworkAgentConfig::class.java),
- eq(NetworkProvider.ID_NONE))
+ verify(mockCm).registerNetworkAgent(
+ any(),
+ argThat<NetworkInfo> { it.detailedState == NetworkInfo.DetailedState.CONNECTING },
+ any(LinkProperties::class.java),
+ any(NetworkCapabilities::class.java),
+ any(NetworkScore::class.java),
+ any(NetworkAgentConfig::class.java),
+ eq(NetworkProvider.ID_NONE)
+ )
}
@Test
@@ -1079,8 +1124,10 @@
@Test
fun testValidationStatus() = createNetworkAgentWithFakeCS().let { agent ->
val uri = Uri.parse("http://www.google.com")
- mFakeConnectivityService.agent.onValidationStatusChanged(VALID_NETWORK,
- uri.toString())
+ mFakeConnectivityService.agent.onValidationStatusChanged(
+ VALID_NETWORK,
+ uri.toString()
+ )
agent.expectCallback<OnValidationStatus>().let {
assertEquals(it.status, VALID_NETWORK)
assertEquals(it.uri, uri)
@@ -1155,7 +1202,8 @@
}
assertFailsWith<IllegalArgumentException> {
agentWeaker.setLingerDuration(Duration.ofMillis(
- NetworkAgent.MIN_LINGER_TIMER_MS.toLong() - 1))
+ NetworkAgent.MIN_LINGER_TIMER_MS.toLong() - 1
+ ))
}
// Verify valid linger timer can be set, but it should not take effect since the network
// is still needed.
@@ -1165,11 +1213,14 @@
agentWeaker.setLingerDuration(Duration.ofMillis(NetworkAgent.MIN_LINGER_TIMER_MS.toLong()))
// Make a listener which can observe agentWeaker lost later.
val callbackWeaker = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
- registerNetworkCallback(NetworkRequest.Builder()
+ registerNetworkCallback(
+ NetworkRequest.Builder()
.clearCapabilities()
.addTransportType(TRANSPORT_TEST)
.setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(specifierWeaker))
- .build(), callbackWeaker)
+ .build(),
+ callbackWeaker
+ )
callbackWeaker.expectAvailableCallbacks(agentWeaker.network!!)
// Connect the agentStronger with a score better than agentWeaker. Verify the callback for
@@ -1186,8 +1237,10 @@
val expectedRemainingLingerDuration = lingerStart +
NetworkAgent.MIN_LINGER_TIMER_MS.toLong() - SystemClock.elapsedRealtime()
// If the available callback is too late. The remaining duration will be reduced.
- assertTrue(expectedRemainingLingerDuration > 0,
- "expected remaining linger duration is $expectedRemainingLingerDuration")
+ assertTrue(
+ expectedRemainingLingerDuration > 0,
+ "expected remaining linger duration is $expectedRemainingLingerDuration"
+ )
callbackWeaker.assertNoCallback(expectedRemainingLingerDuration)
callbackWeaker.expect<Lost>(agentWeaker.network!!)
}
@@ -1205,20 +1258,24 @@
assertEquals(imsi, testNetworkSnapshot!!.subscriberId)
}
- @Test
- @IgnoreUpTo(Build.VERSION_CODES.R)
// TODO: Refactor helper functions to util class and move this test case to
// {@link android.net.cts.ConnectivityManagerTest}.
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
fun testRegisterBestMatchingNetworkCallback() {
// Register best matching network callback with additional condition that will be
// exercised later. This assumes the test network agent has NOT_VCN_MANAGED in it and
// does not have NET_CAPABILITY_TEMPORARILY_NOT_METERED.
val bestMatchingCb = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
- registerBestMatchingNetworkCallback(NetworkRequest.Builder()
+ registerBestMatchingNetworkCallback(
+ NetworkRequest.Builder()
.clearCapabilities()
.addTransportType(TRANSPORT_TEST)
.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
- .build(), bestMatchingCb, mHandlerThread.threadHandler)
+ .build(),
+ bestMatchingCb,
+ mHandlerThread.threadHandler
+ )
val (agent1, _) = createConnectedNetworkAgent(specifier = "AGENT-1")
bestMatchingCb.expectAvailableThenValidatedCallbacks(agent1.network!!)
@@ -1296,8 +1353,10 @@
}
fun assertNoCallback() {
- assertNull(history.poll(NO_CALLBACK_TIMEOUT),
- "Callback received")
+ assertNull(
+ history.poll(NO_CALLBACK_TIMEOUT),
+ "Callback received"
+ )
}
}
@@ -1312,7 +1371,8 @@
private fun setupForQosDatagram() = setupForQosCallbackTest {
agent: TestableNetworkAgent -> DatagramSocket(
- InetSocketAddress(InetAddress.getLoopbackAddress(), 0))
+ InetSocketAddress(InetAddress.getLoopbackAddress(), 0)
+ )
.also { assertNotNull(agent.network?.bindSocket(it)) }
}
@@ -1345,7 +1405,8 @@
assertFailsWith<QosCallbackRegistrationException>(
"The same callback cannot be " +
- "registered more than once without first being unregistered") {
+ "registered more than once without first being unregistered"
+ ) {
mCM.registerQosCallback(info, executor, qosCallback)
}
} finally {
@@ -1438,8 +1499,10 @@
qosCallback.expectCallback<OnQosSessionAvailable>()
// Check that onError is coming through correctly
- agent.sendQosCallbackError(callbackId,
- QosCallbackException.EX_TYPE_FILTER_NOT_SUPPORTED)
+ agent.sendQosCallbackError(
+ callbackId,
+ QosCallbackException.EX_TYPE_FILTER_NOT_SUPPORTED
+ )
qosCallback.expectCallback<OnError> {
it.ex.cause is UnsupportedOperationException
}
@@ -1534,13 +1597,20 @@
val remoteAddresses = ArrayList<InetSocketAddress>()
remoteAddresses.add(InetSocketAddress(REMOTE_ADDRESS, 80))
return EpsBearerQosSessionAttributes(
- qci, 2, 3, 4, 5,
- remoteAddresses
+ qci,
+ 2,
+ 3,
+ 4,
+ 5,
+ remoteAddresses
)
}
- fun sendAndExpectUdpPacket(net: Network,
- reader: PollPacketReader, iface: TestNetworkInterface) {
+ fun sendAndExpectUdpPacket(
+ net: Network,
+ reader: PollPacketReader,
+ iface: TestNetworkInterface
+ ) {
val s = Os.socket(AF_INET6, SOCK_DGRAM, 0)
net.bindSocket(s)
val content = ByteArray(16)
@@ -1553,8 +1623,11 @@
it[IPV6_PROTOCOL_OFFSET].toInt() == IPPROTO_UDP &&
Arrays.equals(content, it.copyOfRange(udpStart, udpStart + content.size))
}
- assertNotNull(match, "Did not receive matching packet on ${iface.interfaceName} " +
- " after ${DEFAULT_TIMEOUT_MS}ms")
+ assertNotNull(
+ match,
+ "Did not receive matching packet on ${iface.interfaceName} " +
+ " after ${DEFAULT_TIMEOUT_MS}ms"
+ )
}
fun createInterfaceAndReader(): Triple<TestNetworkInterface, PollPacketReader, LinkProperties> {
@@ -1750,8 +1823,10 @@
assertNotNull(wifiSpecifier)
assertTrue(wifiSpecifier is EthernetNetworkSpecifier)
- val wifiNc = makeTestNetworkCapabilities(wifiSpecifier.interfaceName,
- intArrayOf(TRANSPORT_WIFI))
+ val wifiNc = makeTestNetworkCapabilities(
+ wifiSpecifier.interfaceName,
+ intArrayOf(TRANSPORT_WIFI)
+ )
wifiAgent.sendNetworkCapabilities(wifiNc)
val wifiLp = mCM.getLinkProperties(wifiNetwork)!!
val newRoute = RouteInfo(IpPrefix("192.0.2.42/24"))
@@ -1822,8 +1897,12 @@
val nc = makeTestNetworkCapabilities(ifName, transports).also {
if (transports.contains(TRANSPORT_VPN)) {
val sessionId = "NetworkAgentTest-${Process.myPid()}"
- it.setTransportInfo(VpnTransportInfo(VpnManager.TYPE_VPN_PLATFORM, sessionId,
- /*bypassable=*/ false, /*longLivedTcpConnectionsExpensive=*/ false))
+ it.setTransportInfo(VpnTransportInfo(
+ VpnManager.TYPE_VPN_PLATFORM,
+ sessionId,
+ /*bypassable=*/ false,
+ /*longLivedTcpConnectionsExpensive=*/ false
+ ))
it.underlyingNetworks = listOf()
}
}
@@ -1868,9 +1947,11 @@
listenCallback.expect<Available>(network)
requestCallback.expect<CapabilitiesChanged>(network) { it.caps.hasCapability(
- NET_CAPABILITY_TEMPORARILY_NOT_METERED) }
+ NET_CAPABILITY_TEMPORARILY_NOT_METERED
+ ) }
listenCallback.expect<CapabilitiesChanged>(network) { it.caps.hasCapability(
- NET_CAPABILITY_TEMPORARILY_NOT_METERED) }
+ NET_CAPABILITY_TEMPORARILY_NOT_METERED
+ ) }
requestCallback.expect<LinkPropertiesChanged>(network) { it.lp.equals(lp) }
listenCallback.expect<LinkPropertiesChanged>(network) { it.lp.equals(lp) }
@@ -1896,7 +1977,8 @@
fun testNativeNetworkCreation_PhysicalNetwork() {
doTestNativeNetworkCreation(
expectCreatedImmediately = SHOULD_CREATE_NETWORKS_IMMEDIATELY,
- intArrayOf(TRANSPORT_CELLULAR))
+ intArrayOf(TRANSPORT_CELLULAR)
+ )
}
@Test
diff --git a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
index 1ca5a77..2fb140a 100644
--- a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
@@ -33,6 +33,7 @@
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
import static com.google.common.truth.Truth.assertThat;
import static junit.framework.Assert.fail;
@@ -103,6 +104,16 @@
}
}
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ @Test
+ public void testParceling() {
+ NetworkCapabilities nc = new NetworkCapabilities.Builder().build();
+ NetworkRequest request = new NetworkRequest(nc, TYPE_NONE, 42 /* rId */,
+ NetworkRequest.Type.RESERVATION);
+
+ assertParcelingIsLossless(request);
+ }
+
@Test
public void testCapabilities() {
assertTrue(new NetworkRequest.Builder().addCapability(NET_CAPABILITY_MMS).build()
diff --git a/tests/cts/net/src/android/net/cts/NetworkReservationTest.kt b/tests/cts/net/src/android/net/cts/NetworkReservationTest.kt
new file mode 100644
index 0000000..f05bf15
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NetworkReservationTest.kt
@@ -0,0 +1,140 @@
+/*
+ * 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 android.net.cts
+
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.Manifest.permission.NETWORK_SETTINGS
+import android.net.ConnectivityManager
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
+import android.net.NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS
+import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
+import android.net.NetworkCapabilities.TRANSPORT_TEST
+import android.net.NetworkProvider
+import android.net.NetworkRequest
+import android.net.NetworkScore
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import android.platform.test.annotations.AppModeFull
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Reserved
+import com.android.testutils.RecorderCallback.CallbackEntry.Unavailable
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.TestableNetworkOfferCallback
+import com.android.testutils.runAsShell
+import kotlin.test.assertEquals
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val TAG = "NetworkReservationTest"
+
+private val NETWORK_SCORE = NetworkScore.Builder().build()
+private val ETHERNET_CAPS = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_ETHERNET)
+ .addTransportType(TRANSPORT_TEST)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_CONGESTED)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .build()
+private val BLANKET_CAPS = NetworkCapabilities(ETHERNET_CAPS).apply {
+ reservationId = RES_ID_MATCH_ALL_RESERVATIONS
+}
+private val ETHERNET_REQUEST = NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_ETHERNET)
+ .addTransportType(TRANSPORT_TEST)
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .build()
+private const val TIMEOUT_MS = 5_000L
+private const val NO_CB_TIMEOUT_MS = 200L
+
+// TODO: integrate with CSNetworkReservationTest and move to common tests.
+@AppModeFull(reason = "CHANGE_NETWORK_STATE, MANAGE_TEST_NETWORKS not grantable to instant apps")
+@ConnectivityModuleTest
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class NetworkReservationTest {
+ private val context = InstrumentationRegistry.getInstrumentation().context
+ private val cm = context.getSystemService(ConnectivityManager::class.java)!!
+ private val handlerThread = HandlerThread("$TAG handler thread").apply { start() }
+ private val handler = Handler(handlerThread.looper)
+ private val provider = NetworkProvider(context, handlerThread.looper, TAG)
+
+ @Before
+ fun setUp() {
+ runAsShell(NETWORK_SETTINGS) {
+ cm.registerNetworkProvider(provider)
+ }
+ }
+
+ @After
+ fun tearDown() {
+ runAsShell(NETWORK_SETTINGS) {
+ // unregisterNetworkProvider unregisters all associated NetworkOffers.
+ cm.unregisterNetworkProvider(provider)
+ }
+ handlerThread.quitSafely()
+ handlerThread.join()
+ }
+
+ fun NetworkCapabilities.copyWithReservationId(resId: Int) = NetworkCapabilities(this).also {
+ it.reservationId = resId
+ }
+
+ @Test
+ fun testReserveNetwork() {
+ // register blanket offer
+ val blanketOffer = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ runAsShell(MANAGE_TEST_NETWORKS) {
+ provider.registerNetworkOffer(NETWORK_SCORE, BLANKET_CAPS, handler::post, blanketOffer)
+ }
+
+ val cb = TestableNetworkCallback()
+ cm.reserveNetwork(ETHERNET_REQUEST, handler, cb)
+
+ // validate the reservation matches the blanket offer.
+ val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+ val reservationId = reservationReq.networkCapabilities.reservationId
+
+ // bring up reserved reservation offer
+ val reservedCaps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+ val reservedOffer = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ runAsShell(MANAGE_TEST_NETWORKS) {
+ provider.registerNetworkOffer(NETWORK_SCORE, reservedCaps, handler::post, reservedOffer)
+ }
+
+ // validate onReserved was sent to the app
+ val appObservedCaps = cb.expect<Reserved>().caps
+ assertEquals(reservedCaps, appObservedCaps)
+
+ // validate the reservation matches the reserved offer.
+ reservedOffer.expectOnNetworkNeeded(reservedCaps)
+
+ // reserved offer goes away
+ provider.unregisterNetworkOffer(reservedOffer)
+ cb.expect<Unavailable>()
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index c981a1b..ee31f1a 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -22,14 +22,10 @@
import android.net.ConnectivityManager.NetworkCallback
import android.net.DnsResolver
import android.net.InetAddresses.parseNumericAddress
-import android.net.LinkAddress
-import android.net.LinkProperties
import android.net.LocalSocket
import android.net.LocalSocketAddress
import android.net.MacAddress
import android.net.Network
-import android.net.NetworkAgentConfig
-import android.net.NetworkCapabilities
import android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED
import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
@@ -53,16 +49,10 @@
import android.os.HandlerThread
import android.platform.test.annotations.AppModeFull
import android.provider.DeviceConfig.NAMESPACE_TETHERING
-import android.system.ErrnoException
-import android.system.Os
-import android.system.OsConstants.AF_INET6
-import android.system.OsConstants.EADDRNOTAVAIL
-import android.system.OsConstants.ENETUNREACH
import android.system.OsConstants.ETH_P_IPV6
import android.system.OsConstants.IPPROTO_IPV6
import android.system.OsConstants.IPPROTO_UDP
import android.system.OsConstants.RT_SCOPE_LINK
-import android.system.OsConstants.SOCK_DGRAM
import android.util.Log
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
@@ -106,7 +96,6 @@
import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
import com.android.testutils.TestDnsPacket
import com.android.testutils.TestableNetworkAgent
-import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnNetworkCreated
import com.android.testutils.TestableNetworkCallback
import com.android.testutils.assertEmpty
import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk30
@@ -247,16 +236,12 @@
val tnm = context.getSystemService(TestNetworkManager::class.java)!!
val iface = tnm.createTapInterface()
val cb = TestableNetworkCallback()
- val testNetworkSpecifier = TestNetworkSpecifier(iface.interfaceName)
cm.requestNetwork(
- NetworkRequest.Builder()
- .removeCapability(NET_CAPABILITY_TRUSTED)
- .addTransportType(TRANSPORT_TEST)
- .setNetworkSpecifier(testNetworkSpecifier)
- .build(),
+ TestableNetworkAgent.makeNetworkRequestForInterface(iface.interfaceName),
cb
)
- val agent = registerTestNetworkAgent(iface.interfaceName)
+ val agent = TestableNetworkAgent.createOnInterface(context, handlerThread.looper,
+ iface.interfaceName, TIMEOUT_MS)
val network = agent.network ?: fail("Registered agent should have a network")
cb.eventuallyExpect<LinkPropertiesChanged>(TIMEOUT_MS) {
@@ -271,57 +256,6 @@
return TestTapNetwork(iface, cb, agent, network)
}
- private fun registerTestNetworkAgent(ifaceName: String): TestableNetworkAgent {
- val lp = LinkProperties().apply {
- interfaceName = ifaceName
- }
- val agent = TestableNetworkAgent(
- context,
- handlerThread.looper,
- NetworkCapabilities().apply {
- removeCapability(NET_CAPABILITY_TRUSTED)
- addTransportType(TRANSPORT_TEST)
- setNetworkSpecifier(TestNetworkSpecifier(ifaceName))
- },
- lp,
- NetworkAgentConfig.Builder().build()
- )
- val network = agent.register()
- agent.markConnected()
- agent.expectCallback<OnNetworkCreated>()
-
- // Wait until the link-local address can be used. Address flags are not available without
- // elevated permissions, so check that bindSocket works.
- PollingCheck.check("No usable v6 address on interface after $TIMEOUT_MS ms", TIMEOUT_MS) {
- // To avoid race condition between socket connection succeeding and interface returning
- // a non-empty address list. Verify that interface returns a non-empty list, before
- // trying the socket connection.
- if (NetworkInterface.getByName(ifaceName).interfaceAddresses.isEmpty()) {
- return@check false
- }
-
- val sock = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP)
- tryTest {
- network.bindSocket(sock)
- Os.connect(sock, parseNumericAddress("ff02::fb%$ifaceName"), 12345)
- true
- }.catch<ErrnoException> {
- if (it.errno != ENETUNREACH && it.errno != EADDRNOTAVAIL) {
- throw it
- }
- false
- } cleanup {
- Os.close(sock)
- }
- }
-
- lp.setLinkAddresses(NetworkInterface.getByName(ifaceName).interfaceAddresses.map {
- LinkAddress(it.address, it.networkPrefixLength.toInt())
- })
- agent.sendLinkProperties(lp)
- return agent
- }
-
private fun makeTestServiceInfo(network: Network? = null) = NsdServiceInfo().also {
it.serviceType = serviceType
it.serviceName = serviceName
@@ -576,7 +510,9 @@
assertEquals(testNetwork1.network, serviceLost.serviceInfo.network)
val newAgent = runAsShell(MANAGE_TEST_NETWORKS) {
- registerTestNetworkAgent(testNetwork1.iface.interfaceName)
+ TestableNetworkAgent.createOnInterface(context, handlerThread.looper,
+ testNetwork1.iface.interfaceName,
+ TIMEOUT_MS)
}
val newNetwork = newAgent.network ?: fail("Registered agent should have a network")
val serviceDiscovered3 = discoveryRecord.expectCallback<ServiceFound>()
diff --git a/tests/cts/net/src/android/net/cts/TrafficStatsTest.java b/tests/cts/net/src/android/net/cts/TrafficStatsTest.java
index bd9e03c..f5198e3 100755
--- a/tests/cts/net/src/android/net/cts/TrafficStatsTest.java
+++ b/tests/cts/net/src/android/net/cts/TrafficStatsTest.java
@@ -24,6 +24,8 @@
import android.util.Log;
import android.util.Range;
+import com.android.testutils.ConnectivityDiagnosticsCollector;
+
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -94,11 +96,12 @@
long tcpPacketToIpBytes(long packetCount, long bytes) {
// ip header + tcp header + data.
- // Tcp header is mostly 32. Syn has different tcp options -> 40. Don't care.
+ // Tcp header is mostly 32. Syn has different tcp options -> 40.
return packetCount * (20 + 32 + bytes);
}
@AppModeFull(reason = "Socket cannot bind in instant app mode")
+ @ConnectivityDiagnosticsCollector.CollectTcpdumpOnFailure
public void testTrafficStatsForLocalhost() throws IOException {
final long mobileTxPacketsBefore = TrafficStats.getMobileTxPackets();
final long mobileRxPacketsBefore = TrafficStats.getMobileRxPackets();
@@ -224,9 +227,15 @@
- uidTxDeltaPackets;
final long deltaRxOtherPackets = (totalRxPacketsAfter - totalRxPacketsBefore)
- uidRxDeltaPackets;
- if (deltaTxOtherPackets > 0 || deltaRxOtherPackets > 0) {
+ final long deltaTxOtherPktBytes = (totalTxBytesAfter - totalTxBytesBefore)
+ - uidTxDeltaBytes;
+ final long deltaRxOtherPktBytes = (totalRxBytesAfter - totalRxBytesBefore)
+ - uidRxDeltaBytes;
+ if (deltaTxOtherPackets != 0 || deltaRxOtherPackets != 0
+ || deltaTxOtherPktBytes != 0 || deltaRxOtherPktBytes != 0) {
Log.i(LOG_TAG, "lingering traffic data: " + deltaTxOtherPackets + "/"
- + deltaRxOtherPackets);
+ + deltaRxOtherPackets + "/" + deltaTxOtherPktBytes
+ + "/" + deltaRxOtherPktBytes);
}
// Check that the per-uid stats obtained from data profiling contain the expected values.
@@ -237,9 +246,9 @@
final long pktBytes = tcpPacketToIpBytes(packetCount, byteCount);
final long pktWithNoDataBytes = tcpPacketToIpBytes(packetCount, 0);
final long minExpExtraPktBytes = tcpPacketToIpBytes(minExpectedExtraPackets, 0);
- final long maxExpExtraPktBytes = tcpPacketToIpBytes(maxExpectedExtraPackets, 0);
- final long deltaTxOtherPktBytes = tcpPacketToIpBytes(deltaTxOtherPackets, 0);
- final long deltaRxOtherPktBytes = tcpPacketToIpBytes(deltaRxOtherPackets, 0);
+ // Syn/syn-ack has different tcp options, make tcp header 40 for upper bound estimation.
+ final long maxExpExtraPktBytes = tcpPacketToIpBytes(maxExpectedExtraPackets, 8);
+
assertInRange("txPackets detail", entry.txPackets, packetCount + minExpectedExtraPackets,
uidTxDeltaPackets);
assertInRange("rxPackets detail", entry.rxPackets, packetCount + minExpectedExtraPackets,
@@ -257,32 +266,24 @@
assertInRange("uidrxb", uidRxDeltaBytes, pktBytes + minExpExtraPktBytes,
pktBytes + pktWithNoDataBytes + maxExpExtraPktBytes + deltaRxOtherPktBytes);
assertInRange("iftxp", ifaceTxDeltaPackets, packetCount + minExpectedExtraPackets,
- packetCount + packetCount + maxExpectedExtraPackets);
+ packetCount + packetCount + maxExpectedExtraPackets + deltaTxOtherPackets);
assertInRange("ifrxp", ifaceRxDeltaPackets, packetCount + minExpectedExtraPackets,
- packetCount + packetCount + maxExpectedExtraPackets);
+ packetCount + packetCount + maxExpectedExtraPackets + deltaRxOtherPackets);
assertInRange("iftxb", ifaceTxDeltaBytes, pktBytes + minExpExtraPktBytes,
- pktBytes + pktWithNoDataBytes + maxExpExtraPktBytes);
+ pktBytes + pktWithNoDataBytes + maxExpExtraPktBytes + deltaTxOtherPktBytes);
assertInRange("ifrxb", ifaceRxDeltaBytes, pktBytes + minExpExtraPktBytes,
- pktBytes + pktWithNoDataBytes + maxExpExtraPktBytes);
+ pktBytes + pktWithNoDataBytes + maxExpExtraPktBytes + deltaRxOtherPktBytes);
// Localhost traffic *does* count against total stats.
// Check the total stats increased after test data transfer over localhost has been made.
- assertTrue("ttxp: " + totalTxPacketsBefore + " -> " + totalTxPacketsAfter,
- totalTxPacketsAfter >= totalTxPacketsBefore + uidTxDeltaPackets);
- assertTrue("trxp: " + totalRxPacketsBefore + " -> " + totalRxPacketsAfter,
- totalRxPacketsAfter >= totalRxPacketsBefore + uidRxDeltaPackets);
- assertTrue("ttxb: " + totalTxBytesBefore + " -> " + totalTxBytesAfter,
- totalTxBytesAfter >= totalTxBytesBefore + uidTxDeltaBytes);
- assertTrue("trxb: " + totalRxBytesBefore + " -> " + totalRxBytesAfter,
- totalRxBytesAfter >= totalRxBytesBefore + uidRxDeltaBytes);
- assertTrue("iftxp: " + ifaceTxPacketsBefore + " -> " + ifaceTxPacketsAfter,
- totalTxPacketsAfter >= totalTxPacketsBefore + ifaceTxDeltaPackets);
- assertTrue("ifrxp: " + ifaceRxPacketsBefore + " -> " + ifaceRxPacketsAfter,
- totalRxPacketsAfter >= totalRxPacketsBefore + ifaceRxDeltaPackets);
- assertTrue("iftxb: " + ifaceTxBytesBefore + " -> " + ifaceTxBytesAfter,
- totalTxBytesAfter >= totalTxBytesBefore + ifaceTxDeltaBytes);
- assertTrue("ifrxb: " + ifaceRxBytesBefore + " -> " + ifaceRxBytesAfter,
- totalRxBytesAfter >= totalRxBytesBefore + ifaceRxDeltaBytes);
+ assertInRange("ttxp", totalTxPacketsAfter,
+ totalTxPacketsBefore + packetCount + minExpectedExtraPackets, Long.MAX_VALUE);
+ assertInRange("trxp", totalRxPacketsAfter,
+ totalRxPacketsBefore + packetCount + minExpectedExtraPackets, Long.MAX_VALUE);
+ assertInRange("ttxb", totalTxBytesAfter,
+ totalTxBytesBefore + pktBytes + minExpExtraPktBytes, Long.MAX_VALUE);
+ assertInRange("trxb", totalRxBytesAfter,
+ totalRxBytesBefore + pktBytes + minExpExtraPktBytes, Long.MAX_VALUE);
// Localhost traffic should *not* count against mobile stats,
// There might be some other traffic, but nowhere near 1MB.
diff --git a/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java b/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java
index 243cd27..75b2814 100644
--- a/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java
+++ b/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java
@@ -22,6 +22,7 @@
import static android.Manifest.permission.TETHER_PRIVILEGED;
import static android.net.TetheringManager.TETHERING_WIFI;
import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_REQUEST;
import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_FAILED;
import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STARTED;
import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STOPPED;
@@ -126,6 +127,67 @@
}
}
+ public static class StopTetheringCallback implements TetheringManager.StopTetheringCallback {
+ private static final int TIMEOUT_MS = 30_000;
+ public static class CallbackValue {
+ public final int error;
+
+ private CallbackValue(final int e) {
+ error = e;
+ }
+
+ public static class OnStopTetheringSucceeded extends CallbackValue {
+ OnStopTetheringSucceeded() {
+ super(TETHER_ERROR_NO_ERROR);
+ }
+ }
+
+ public static class OnStopTetheringFailed extends CallbackValue {
+ OnStopTetheringFailed(final int error) {
+ super(error);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s(%d)", getClass().getSimpleName(), error);
+ }
+ }
+
+ private final ArrayTrackRecord<CallbackValue>.ReadHead mHistory =
+ new ArrayTrackRecord<CallbackValue>().newReadHead();
+
+ @Override
+ public void onStopTetheringSucceeded() {
+ mHistory.add(new CallbackValue.OnStopTetheringSucceeded());
+ }
+
+ @Override
+ public void onStopTetheringFailed(final int error) {
+ mHistory.add(new CallbackValue.OnStopTetheringFailed(error));
+ }
+
+ /**
+ * Verifies that {@link #onStopTetheringSucceeded()} was called
+ */
+ public void verifyStopTetheringSucceeded() {
+ final CallbackValue cv = mHistory.poll(TIMEOUT_MS, c -> true);
+ assertNotNull("No onStopTetheringSucceeded after " + TIMEOUT_MS + " ms", cv);
+ assertTrue("Fail stop tethering:" + cv,
+ cv instanceof CallbackValue.OnStopTetheringSucceeded);
+ }
+
+ /**
+ * Verifies that {@link #onStopTetheringFailed(int)} was called
+ */
+ public void expectStopTetheringFailed(final int expected) {
+ final CallbackValue cv = mHistory.poll(TIMEOUT_MS, c -> true);
+ assertNotNull("No onStopTetheringFailed after " + TIMEOUT_MS + " ms", cv);
+ assertTrue("Expect fail with error code " + expected + ", but received: " + cv,
+ (cv instanceof CallbackValue.OnStopTetheringFailed) && (cv.error == expected));
+ }
+ }
+
private static boolean isRegexMatch(final String[] ifaceRegexs, String iface) {
if (ifaceRegexs == null) fail("ifaceRegexs should not be null");
@@ -574,6 +636,22 @@
expectSoftApDisabled();
}
+ /**
+ * Calls {@link TetheringManager#stopTethering(TetheringRequest, Executor,
+ * TetheringManager.StopTetheringCallback)} and verifies if it succeeded or failed.
+ */
+ public void stopTethering(final TetheringRequest request, boolean expectSuccess) {
+ final StopTetheringCallback stopTetheringCallback = new StopTetheringCallback();
+ runAsShell(TETHER_PRIVILEGED, () -> {
+ mTm.stopTethering(request, c -> c.run() /* executor */, stopTetheringCallback);
+ if (expectSuccess) {
+ stopTetheringCallback.verifyStopTetheringSucceeded();
+ } else {
+ stopTetheringCallback.expectStopTetheringFailed(TETHER_ERROR_UNKNOWN_REQUEST);
+ }
+ });
+ }
+
public void stopAllTethering() {
final TestTetheringEventCallback callback = registerTetheringEventCallback();
try {
diff --git a/tests/cts/netpermission/updatestatspermission/Android.bp b/tests/cts/netpermission/updatestatspermission/Android.bp
index b324dc8..0ff98e7 100644
--- a/tests/cts/netpermission/updatestatspermission/Android.bp
+++ b/tests/cts/netpermission/updatestatspermission/Android.bp
@@ -33,6 +33,7 @@
// Tag this module as a cts test artifact
test_suites: [
+ "automotive-general-tests",
"cts",
"general-tests",
],
diff --git a/tests/cts/tethering/Android.bp b/tests/cts/tethering/Android.bp
index d167836..a1e0797 100644
--- a/tests/cts/tethering/Android.bp
+++ b/tests/cts/tethering/Android.bp
@@ -55,34 +55,6 @@
host_required: ["net-tests-utils-host-common"],
}
-// Tethering CTS tests that target the latest released SDK. These tests can be installed on release
-// devices which has equal or lowner sdk version than target sdk and are useful for qualifying
-// mainline modules on release devices.
-android_test {
- name: "CtsTetheringTestLatestSdk",
- defaults: [
- "ConnectivityTestsLatestSdkDefaults",
- "CtsTetheringTestDefaults",
- ],
-
- min_sdk_version: "30",
-
- static_libs: [
- "TetheringIntegrationTestsLatestSdkLib",
- ],
-
- test_suites: [
- "general-tests",
- "mts-tethering",
- ],
-
- test_config_template: "AndroidTestTemplate.xml",
-
- // Include both the 32 and 64 bit versions
- compile_multilib: "both",
- jarjar_rules: ":NetworkStackJarJarRules",
-}
-
// Tethering CTS tests for development and release. These tests always target the platform SDK
// version, and are subject to all the restrictions appropriate to that version. Before SDK
// finalization, these tests have a min_sdk_version of 10000, but they can still be installed on
diff --git a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
index 5e94c06..9e49926 100644
--- a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
+++ b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
@@ -45,6 +45,7 @@
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeFalse;
@@ -87,9 +88,11 @@
import com.android.modules.utils.build.SdkLevel;
import com.android.net.flags.Flags;
import com.android.testutils.ParcelUtils;
+import com.android.testutils.com.android.testutils.CarrierConfigRule;
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -98,13 +101,14 @@
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
@RunWith(AndroidJUnit4.class)
public class TetheringManagerTest {
+ @Rule
+ public final CarrierConfigRule mCarrierConfigRule = new CarrierConfigRule();
private Context mContext;
@@ -387,18 +391,21 @@
mCtsTetheringUtils.stopWifiTethering(tetherEventCallback);
- try {
- final int ret = runAsShell(TETHER_PRIVILEGED, () -> mTM.tether(wifiTetheringIface));
- // There is no guarantee that the wifi interface will be available after disabling
- // the hotspot, so don't fail the test if the call to tether() fails.
- if (ret == TETHER_ERROR_NO_ERROR) {
- // If calling #tether successful, there is a callback to tell the result of
- // tethering setup.
- tetherEventCallback.expectErrorOrTethered(
- new TetheringInterface(TETHERING_WIFI, wifiTetheringIface));
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ try {
+ final int ret = runAsShell(TETHER_PRIVILEGED,
+ () -> mTM.tether(wifiTetheringIface));
+ // There is no guarantee that the wifi interface will be available after
+ // disabling the hotspot, so don't fail the test if the call to tether() fails.
+ if (ret == TETHER_ERROR_NO_ERROR) {
+ // If calling #tether successful, there is a callback to tell the result of
+ // tethering setup.
+ tetherEventCallback.expectErrorOrTethered(
+ new TetheringInterface(TETHERING_WIFI, wifiTetheringIface));
+ }
+ } finally {
+ runAsShell(TETHER_PRIVILEGED, () -> mTM.untether(wifiTetheringIface));
}
- } finally {
- runAsShell(TETHER_PRIVILEGED, () -> mTM.untether(wifiTetheringIface));
}
} finally {
mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
@@ -450,14 +457,26 @@
@Test
public void testStopTetheringRequest() throws Exception {
- TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI).build();
- Executor executor = Runnable::run;
- TetheringManager.StopTetheringCallback callback =
- new TetheringManager.StopTetheringCallback() {};
+ assumeTrue(isTetheringWithSoftApConfigEnabled());
+ final TestTetheringEventCallback tetherEventCallback =
+ mCtsTetheringUtils.registerTetheringEventCallback();
try {
- mTM.stopTethering(request, executor, callback);
- fail("stopTethering should throw UnsupportedOperationException");
- } catch (UnsupportedOperationException expect) { }
+ tetherEventCallback.assumeWifiTetheringSupported(mContext);
+
+ // stopTethering without any tethering active should fail.
+ TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI).build();
+ mCtsTetheringUtils.stopTethering(request, false /* expectSuccess */);
+
+ // Start wifi tethering
+ mCtsTetheringUtils.startWifiTethering(tetherEventCallback);
+
+ // stopTethering should succeed now that there's a request.
+ mCtsTetheringUtils.stopTethering(request, true /* expectSuccess */);
+ tetherEventCallback.expectNoTetheringActive();
+ } finally {
+ mCtsTetheringUtils.stopAllTethering();
+ mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
+ }
}
private boolean isTetheringWithSoftApConfigEnabled() {
@@ -547,22 +566,13 @@
// Override carrier config to ignore entitlement check.
final PersistableBundle bundle = new PersistableBundle();
bundle.putBoolean(CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL, false);
- overrideCarrierConfig(bundle);
+ mCarrierConfigRule.addConfigOverrides(
+ SubscriptionManager.getDefaultSubscriptionId(), bundle);
// Verify that requestLatestTetheringEntitlementResult() can get entitlement
// result TETHER_ERROR_NO_ERROR due to provisioning bypassed.
assertEntitlementResult(listener -> mTM.requestLatestTetheringEntitlementResult(
TETHERING_WIFI, false, c -> c.run(), listener), TETHER_ERROR_NO_ERROR);
-
- // Reset carrier config.
- overrideCarrierConfig(null);
- }
-
- private void overrideCarrierConfig(PersistableBundle bundle) {
- final CarrierConfigManager configManager = (CarrierConfigManager) mContext
- .getSystemService(Context.CARRIER_CONFIG_SERVICE);
- final int subId = SubscriptionManager.getDefaultSubscriptionId();
- runAsShell(MODIFY_PHONE_STATE, () -> configManager.overrideConfig(subId, bundle));
}
private boolean isTetheringApnRequired() {
@@ -623,4 +633,11 @@
}
}
}
+
+ @Test
+ public void testLegacyTetherApisThrowUnsupportedOperationExceptionAfterV() {
+ assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM);
+ assertThrows(UnsupportedOperationException.class, () -> mTM.tether("iface"));
+ assertThrows(UnsupportedOperationException.class, () -> mTM.untether("iface"));
+ }
}
diff --git a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
index 06bdca6..375d604 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
@@ -16,6 +16,7 @@
package com.android.server.net.integrationtests
+import android.Manifest.permission
import android.app.usage.NetworkStatsManager
import android.content.ComponentName
import android.content.Context
@@ -66,6 +67,7 @@
import com.android.testutils.DeviceInfoUtils
import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.runAsShell
import com.android.testutils.tryTest
import java.util.function.BiConsumer
import java.util.function.Consumer
@@ -208,7 +210,9 @@
networkStackClient = TestNetworkStackClient(realContext)
networkStackClient.start()
- service = TestConnectivityService(TestDependencies())
+ service = runAsShell(permission.OBSERVE_GRANT_REVOKE_PERMISSIONS) {
+ TestConnectivityService(TestDependencies())
+ }
cm = ConnectivityManager(context, service)
context.addMockSystemService(Context.CONNECTIVITY_SERVICE, cm)
context.addMockSystemService(Context.NETWORK_STATS_SERVICE, statsManager)
diff --git a/tests/native/connectivity_native_test/Android.bp b/tests/native/connectivity_native_test/Android.bp
index 39a08fa..02ac3c5 100644
--- a/tests/native/connectivity_native_test/Android.bp
+++ b/tests/native/connectivity_native_test/Android.bp
@@ -24,7 +24,6 @@
"libcom.android.tethering.connectivity_native",
"liblog",
"libnetutils",
- "libprocessgroup",
],
static_libs: [
"connectivity_native_aidl_interface-lateststable-ndk",
diff --git a/tests/unit/java/android/net/ConnectivityManagerTest.java b/tests/unit/java/android/net/ConnectivityManagerTest.java
index 9a77c89..b415382 100644
--- a/tests/unit/java/android/net/ConnectivityManagerTest.java
+++ b/tests/unit/java/android/net/ConnectivityManagerTest.java
@@ -44,6 +44,7 @@
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.nullable;
@@ -65,6 +66,7 @@
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.net.ConnectivityManager.NetworkCallback;
+import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.Handler;
@@ -669,4 +671,12 @@
// No callbacks overridden -> do not use the optimization
eq(~0));
}
+
+ @Test
+ public void testLegacyTetherApisThrowUnsupportedOperationExceptionAfterV() {
+ assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM);
+ final ConnectivityManager manager = new ConnectivityManager(mCtx, mService);
+ assertThrows(UnsupportedOperationException.class, () -> manager.tether("iface"));
+ assertThrows(UnsupportedOperationException.class, () -> manager.untether("iface"));
+ }
}
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index c1c15ca..73eb24d 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -71,6 +71,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
@@ -99,6 +100,7 @@
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.IBpfMap;
+import com.android.net.module.util.Struct.Bool;
import com.android.net.module.util.Struct.S32;
import com.android.net.module.util.Struct.U32;
import com.android.net.module.util.Struct.U8;
@@ -106,6 +108,7 @@
import com.android.net.module.util.bpf.CookieTagMapValue;
import com.android.net.module.util.bpf.IngressDiscardKey;
import com.android.net.module.util.bpf.IngressDiscardValue;
+import com.android.net.module.util.bpf.LocalNetAccessKey;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
@@ -171,6 +174,10 @@
private final IBpfMap<S32, UidOwnerValue> mUidOwnerMap =
new TestBpfMap<>(S32.class, UidOwnerValue.class);
private final IBpfMap<S32, U8> mUidPermissionMap = new TestBpfMap<>(S32.class, U8.class);
+ private final IBpfMap<U32, Bool> mLocalNetBlockedUidMap =
+ new TestBpfMap<>(U32.class, Bool.class);
+ private final IBpfMap<LocalNetAccessKey, Bool> mLocalNetAccessMap =
+ new TestBpfMap<>(LocalNetAccessKey.class, Bool.class);
private final IBpfMap<CookieTagMapKey, CookieTagMapValue> mCookieTagMap =
spy(new TestBpfMap<>(CookieTagMapKey.class, CookieTagMapValue.class));
private final IBpfMap<S32, U8> mDataSaverEnabledMap = new TestBpfMap<>(S32.class, U8.class);
@@ -189,6 +196,8 @@
CURRENT_STATS_MAP_CONFIGURATION_KEY, new U32(STATS_SELECT_MAP_A));
BpfNetMaps.setUidOwnerMapForTest(mUidOwnerMap);
BpfNetMaps.setUidPermissionMapForTest(mUidPermissionMap);
+ BpfNetMaps.setLocalNetAccessMapForTest(mLocalNetAccessMap);
+ BpfNetMaps.setLocalNetBlockedUidMapForTest(mLocalNetBlockedUidMap);
BpfNetMaps.setCookieTagMapForTest(mCookieTagMap);
BpfNetMaps.setDataSaverEnabledMapForTest(mDataSaverEnabledMap);
mDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, new U8(DATA_SAVER_DISABLED));
@@ -235,6 +244,138 @@
}
@Test
+ @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testAddLocalNetAccessBeforeV() {
+ assertThrows(UnsupportedOperationException.class, () ->
+ mBpfNetMaps.addLocalNetAccess(0, TEST_IF_NAME, Inet6Address.ANY, 0, 0, true));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testAddLocalNetAccessAfterV() throws Exception {
+ assertTrue(mLocalNetAccessMap.isEmpty());
+
+ mBpfNetMaps.addLocalNetAccess(160, TEST_IF_NAME,
+ Inet4Address.getByName("196.68.0.0"), 0, 0, true);
+
+ assertNotNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("196.68.0.0"), 0, 0)));
+ assertNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("100.68.0.0"), 0, 0)));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testAddLocalNetAccessAfterVWithIncorrectInterface() throws Exception {
+ assertTrue(mLocalNetAccessMap.isEmpty());
+
+ mBpfNetMaps.addLocalNetAccess(160, "wlan2",
+ Inet4Address.getByName("196.68.0.0"), 0, 0, true);
+
+ assertTrue(mLocalNetAccessMap.isEmpty());
+ }
+
+ @Test
+ @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testGetLocalNetAccessBeforeV() {
+ assertThrows(UnsupportedOperationException.class, () ->
+ mBpfNetMaps.getLocalNetAccess(0, TEST_IF_NAME, Inet6Address.ANY, 0, 0));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testGetLocalNetAccessAfterV() throws Exception {
+ assertTrue(mLocalNetAccessMap.isEmpty());
+
+ mLocalNetAccessMap.updateEntry(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("196.68.0.0"), 0, 0),
+ new Bool(false));
+
+ assertNotNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("196.68.0.0"), 0, 0)));
+
+ assertFalse(mBpfNetMaps.getLocalNetAccess(160, TEST_IF_NAME,
+ Inet4Address.getByName("196.68.0.0"), 0, 0));
+ assertTrue(mBpfNetMaps.getLocalNetAccess(160, TEST_IF_NAME,
+ Inet4Address.getByName("100.68.0.0"), 0, 0));
+ }
+
+ @Test
+ @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testAddUidToLocalNetBlockMapBeforeV() {
+ assertThrows(UnsupportedOperationException.class, () ->
+ mBpfNetMaps.addUidToLocalNetBlockMap(0));
+ }
+
+ @Test
+ @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testIsUidPresentInLocalNetBlockMapBeforeV() {
+ assertThrows(UnsupportedOperationException.class, () ->
+ mBpfNetMaps.getUidValueFromLocalNetBlockMap(0));
+ }
+
+ @Test
+ @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testRemoveUidFromLocalNetBlockMapBeforeV() {
+ assertThrows(UnsupportedOperationException.class, () ->
+ mBpfNetMaps.removeUidFromLocalNetBlockMap(0));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testAddUidFromLocalNetBlockMapAfterV() throws Exception {
+ final int uid0 = TEST_UIDS[0];
+ final int uid1 = TEST_UIDS[1];
+
+ assertTrue(mLocalNetAccessMap.isEmpty());
+
+ mBpfNetMaps.addUidToLocalNetBlockMap(uid0);
+ assertTrue(mLocalNetBlockedUidMap.getValue(new U32(uid0)).val);
+ assertNull(mLocalNetBlockedUidMap.getValue(new U32(uid1)));
+
+ mBpfNetMaps.addUidToLocalNetBlockMap(uid1);
+ assertTrue(mLocalNetBlockedUidMap.getValue(new U32(uid1)).val);
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testIsUidPresentInLocalNetBlockMapAfterV() throws Exception {
+ final int uid0 = TEST_UIDS[0];
+ final int uid1 = TEST_UIDS[1];
+
+ assertTrue(mLocalNetAccessMap.isEmpty());
+
+ mLocalNetBlockedUidMap.updateEntry(new U32(uid0), new Bool(true));
+ assertTrue(mBpfNetMaps.getUidValueFromLocalNetBlockMap(uid0));
+ assertFalse(mBpfNetMaps.getUidValueFromLocalNetBlockMap(uid1));
+
+ mLocalNetBlockedUidMap.updateEntry(new U32(uid1), new Bool(true));
+ assertTrue(mBpfNetMaps.getUidValueFromLocalNetBlockMap(uid1));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testRemoveUidFromLocalNetBlockMapAfterV() throws Exception {
+ final int uid0 = TEST_UIDS[0];
+ final int uid1 = TEST_UIDS[1];
+
+ assertTrue(mLocalNetAccessMap.isEmpty());
+
+ mLocalNetBlockedUidMap.updateEntry(new U32(uid0), new Bool(true));
+ mLocalNetBlockedUidMap.updateEntry(new U32(uid1), new Bool(true));
+
+ assertTrue(mLocalNetBlockedUidMap.getValue(new U32(uid0)).val);
+ assertTrue(mLocalNetBlockedUidMap.getValue(new U32(uid1)).val);
+
+ mBpfNetMaps.removeUidFromLocalNetBlockMap(uid0);
+ assertNull(mLocalNetBlockedUidMap.getValue(new U32(uid0)));
+ assertTrue(mLocalNetBlockedUidMap.getValue(new U32(uid1)).val);
+
+ mBpfNetMaps.removeUidFromLocalNetBlockMap(uid1);
+ assertNull(mLocalNetBlockedUidMap.getValue(new U32(uid1)));
+ }
+
+ @Test
@IgnoreUpTo(Build.VERSION_CODES.S_V2)
public void testIsChainEnabled() throws Exception {
doTestIsChainEnabled(FIREWALL_CHAIN_DOZABLE);
diff --git a/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt b/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt
new file mode 100644
index 0000000..5a7515e
--- /dev/null
+++ b/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.FEATURE_BLUETOOTH_LE
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.TYPE_NONE
+import android.net.L2capNetworkSpecifier
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_6LOWPAN
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_NONE
+import android.net.L2capNetworkSpecifier.ROLE_SERVER
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH
+import android.net.NetworkProvider
+import android.net.NetworkProvider.NetworkOfferCallback
+import android.net.NetworkRequest
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import kotlin.test.assertTrue
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+const val TAG = "L2capNetworkProviderTest"
+
+val RESERVATION_CAPS = NetworkCapabilities.Builder.withoutDefaultCapabilities()
+ .addTransportType(TRANSPORT_BLUETOOTH)
+ .build()
+
+val RESERVATION = NetworkRequest(
+ NetworkCapabilities(RESERVATION_CAPS),
+ TYPE_NONE,
+ 42 /* rId */,
+ NetworkRequest.Type.RESERVATION
+)
+
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class L2capNetworkProviderTest {
+ @Mock private lateinit var context: Context
+ @Mock private lateinit var deps: L2capNetworkProvider.Dependencies
+ @Mock private lateinit var provider: NetworkProvider
+ @Mock private lateinit var cm: ConnectivityManager
+ @Mock private lateinit var pm: PackageManager
+
+ private val handlerThread = HandlerThread("$TAG handler thread").apply { start() }
+ private val handler = Handler(handlerThread.looper)
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ doReturn(provider).`when`(deps).getNetworkProvider(any(), any())
+ doReturn(handlerThread).`when`(deps).getHandlerThread()
+ doReturn(cm).`when`(context).getSystemService(eq(ConnectivityManager::class.java))
+ doReturn(pm).`when`(context).getPackageManager()
+ doReturn(true).`when`(pm).hasSystemFeature(FEATURE_BLUETOOTH_LE)
+ }
+
+ @After
+ fun tearDown() {
+ handlerThread.quitSafely()
+ handlerThread.join()
+ }
+
+ @Test
+ fun testNetworkProvider_registeredWhenSupported() {
+ L2capNetworkProvider(deps, context).start()
+ verify(cm).registerNetworkProvider(eq(provider))
+ verify(provider).registerNetworkOffer(any(), any(), any(), any())
+ }
+
+ @Test
+ fun testNetworkProvider_notRegisteredWhenNotSupported() {
+ doReturn(false).`when`(pm).hasSystemFeature(FEATURE_BLUETOOTH_LE)
+ L2capNetworkProvider(deps, context).start()
+ verify(cm, never()).registerNetworkProvider(eq(provider))
+ }
+
+ fun doTestBlanketOfferIgnoresRequest(request: NetworkRequest) {
+ clearInvocations(provider)
+ L2capNetworkProvider(deps, context).start()
+
+ val blanketOfferCaptor = ArgumentCaptor.forClass(NetworkOfferCallback::class.java)
+ verify(provider).registerNetworkOffer(any(), any(), any(), blanketOfferCaptor.capture())
+
+ blanketOfferCaptor.value.onNetworkNeeded(request)
+ verify(provider).registerNetworkOffer(any(), any(), any(), any())
+ }
+
+ fun doTestBlanketOfferCreatesReservation(
+ request: NetworkRequest,
+ reservation: NetworkCapabilities
+ ) {
+ clearInvocations(provider)
+ L2capNetworkProvider(deps, context).start()
+
+ val blanketOfferCaptor = ArgumentCaptor.forClass(NetworkOfferCallback::class.java)
+ verify(provider).registerNetworkOffer(any(), any(), any(), blanketOfferCaptor.capture())
+
+ blanketOfferCaptor.value.onNetworkNeeded(request)
+
+ val capsCaptor = ArgumentCaptor.forClass(NetworkCapabilities::class.java)
+ verify(provider, times(2)).registerNetworkOffer(any(), capsCaptor.capture(), any(), any())
+
+ assertTrue(reservation.satisfiedByNetworkCapabilities(capsCaptor.value))
+ }
+
+ @Test
+ fun testBlanketOffer_reservationWithoutSpecifier() {
+ val caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
+ .addTransportType(TRANSPORT_BLUETOOTH)
+ .build()
+ val nr = NetworkRequest(caps, TYPE_NONE, 42 /* rId */, NetworkRequest.Type.RESERVATION)
+
+ doTestBlanketOfferIgnoresRequest(nr)
+ }
+
+ @Test
+ fun testBlanketOffer_reservationWithCorrectSpecifier() {
+ var specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+ .build()
+ var caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
+ .addTransportType(TRANSPORT_BLUETOOTH)
+ .setNetworkSpecifier(specifier)
+ .build()
+ var nr = NetworkRequest(caps, TYPE_NONE, 42 /* rId */, NetworkRequest.Type.RESERVATION)
+ doTestBlanketOfferCreatesReservation(nr, caps)
+
+ specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(HEADER_COMPRESSION_NONE)
+ .build()
+ caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
+ .addTransportType(TRANSPORT_BLUETOOTH)
+ .setNetworkSpecifier(specifier)
+ .build()
+ nr = NetworkRequest(caps, TYPE_NONE, 43 /* rId */, NetworkRequest.Type.RESERVATION)
+ doTestBlanketOfferCreatesReservation(nr, caps)
+ }
+
+ @Test
+ fun testBlanketOffer_reservationWithIncorrectSpecifier() {
+ var specifier = L2capNetworkSpecifier.Builder().build()
+ var caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
+ .addTransportType(TRANSPORT_BLUETOOTH)
+ .setNetworkSpecifier(specifier)
+ .build()
+ var nr = NetworkRequest(caps, TYPE_NONE, 42 /* rId */, NetworkRequest.Type.RESERVATION)
+ doTestBlanketOfferIgnoresRequest(nr)
+
+ specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .build()
+ caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
+ .addTransportType(TRANSPORT_BLUETOOTH)
+ .setNetworkSpecifier(specifier)
+ .build()
+ nr = NetworkRequest(caps, TYPE_NONE, 44 /* rId */, NetworkRequest.Type.RESERVATION)
+ doTestBlanketOfferIgnoresRequest(nr)
+
+ specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(HEADER_COMPRESSION_NONE)
+ .setPsm(0x81)
+ .build()
+ caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
+ .addTransportType(TRANSPORT_BLUETOOTH)
+ .setNetworkSpecifier(specifier)
+ .build()
+ nr = NetworkRequest(caps, TYPE_NONE, 45 /* rId */, NetworkRequest.Type.RESERVATION)
+ doTestBlanketOfferIgnoresRequest(nr)
+
+ specifier = L2capNetworkSpecifier.Builder()
+ .setHeaderCompression(HEADER_COMPRESSION_NONE)
+ .build()
+ caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
+ .addTransportType(TRANSPORT_BLUETOOTH)
+ .setNetworkSpecifier(specifier)
+ .build()
+ nr = NetworkRequest(caps, TYPE_NONE, 47 /* rId */, NetworkRequest.Type.RESERVATION)
+ doTestBlanketOfferIgnoresRequest(nr)
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
index 5bde31a..55c68b7 100644
--- a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
@@ -22,6 +22,7 @@
import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
import static android.Manifest.permission.INTERNET;
import static android.Manifest.permission.NETWORK_STACK;
+import static android.Manifest.permission.OBSERVE_GRANT_REVOKE_PERMISSIONS;
import static android.Manifest.permission.UPDATE_DEVICE_STATS;
import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_OEM;
import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_PRODUCT;
@@ -41,6 +42,7 @@
import static android.os.Process.SYSTEM_UID;
import static com.android.server.connectivity.PermissionMonitor.isHigherNetworkPermission;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
import static junit.framework.Assert.fail;
@@ -1235,8 +1237,10 @@
// Use the real context as this test must ensure the *real* system package holds the
// necessary permission.
final Context realContext = InstrumentationRegistry.getContext();
- final PermissionMonitor monitor = new PermissionMonitor(
- realContext, mNetdService, mBpfNetMaps, mHandlerThread);
+ final PermissionMonitor monitor = runAsShell(
+ OBSERVE_GRANT_REVOKE_PERMISSIONS,
+ () -> new PermissionMonitor(realContext, mNetdService, mBpfNetMaps, mHandlerThread)
+ );
final PackageManager manager = realContext.getPackageManager();
final PackageInfo systemInfo = manager.getPackageInfo(REAL_SYSTEM_PACKAGE_NAME,
GET_PERMISSIONS | MATCH_ANY_USER);
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSNetworkReservationTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetworkReservationTest.kt
index 7b6c995..e698930 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSNetworkReservationTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSNetworkReservationTest.kt
@@ -17,12 +17,16 @@
package com.android.server
import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED
import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
-import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_WIFI_P2P
import android.net.NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS
import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
+import android.net.NetworkCapabilities.TRANSPORT_TEST
import android.net.NetworkProvider
+import android.net.NetworkProvider.NetworkOfferCallback
import android.net.NetworkRequest
import android.net.NetworkScore
import android.os.Build
@@ -35,16 +39,26 @@
import com.android.testutils.TestableNetworkOfferCallback.CallbackEntry.OnNetworkNeeded
import kotlin.test.assertEquals
import kotlin.test.assertNull
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-
private val ETHERNET_SCORE = NetworkScore.Builder().build()
private val ETHERNET_CAPS = NetworkCapabilities.Builder()
.addTransportType(TRANSPORT_ETHERNET)
+ .addTransportType(TRANSPORT_TEST)
.addCapability(NET_CAPABILITY_INTERNET)
.addCapability(NET_CAPABILITY_NOT_CONGESTED)
.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .build()
+private val BLANKET_CAPS = NetworkCapabilities(ETHERNET_CAPS).apply {
+ reservationId = RES_ID_MATCH_ALL_RESERVATIONS
+}
+private val ETHERNET_REQUEST = NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_ETHERNET)
+ .addTransportType(TRANSPORT_TEST)
+ .removeCapability(NET_CAPABILITY_TRUSTED)
.build()
private const val TIMEOUT_MS = 5_000L
@@ -53,42 +67,53 @@
@RunWith(DevSdkIgnoreRunner::class)
@IgnoreUpTo(Build.VERSION_CODES.R)
class CSNetworkReservationTest : CSTest() {
+ private lateinit var provider: NetworkProvider
+ private val blanketOffer = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+
+ @Before
+ fun subclassSetUp() {
+ provider = NetworkProvider(context, csHandlerThread.looper, "Ethernet provider")
+ cm.registerNetworkProvider(provider)
+
+ // register a blanket offer for use in tests.
+ provider.registerNetworkOffer(ETHERNET_SCORE, BLANKET_CAPS, blanketOffer)
+ }
+
fun NetworkCapabilities.copyWithReservationId(resId: Int) = NetworkCapabilities(this).also {
it.reservationId = resId
}
+ fun NetworkProvider.registerNetworkOffer(
+ score: NetworkScore,
+ caps: NetworkCapabilities,
+ cb: NetworkOfferCallback
+ ) {
+ registerNetworkOffer(score, caps, {r -> r.run()}, cb)
+ }
+
@Test
fun testReservationRequest() {
- val provider = NetworkProvider(context, csHandlerThread.looper, "Ethernet provider")
- val blanketOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
-
- cm.registerNetworkProvider(provider)
-
- val blanketCaps = ETHERNET_CAPS.copyWithReservationId(RES_ID_MATCH_ALL_RESERVATIONS)
- provider.registerNetworkOffer(ETHERNET_SCORE, blanketCaps, {r -> r.run()}, blanketOfferCb)
-
- val req = NetworkRequest.Builder().addTransportType(TRANSPORT_ETHERNET).build()
val cb = TestableNetworkCallback()
- cm.reserveNetwork(req, csHandler, cb)
+ cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
// validate the reservation matches the blanket offer.
- val reservationReq = blanketOfferCb.expectOnNetworkNeeded(blanketCaps).request
+ val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
val reservationId = reservationReq.networkCapabilities.reservationId
- // bring up specific reservation offer
- val specificCaps = ETHERNET_CAPS.copyWithReservationId(reservationId)
- val specificOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
- provider.registerNetworkOffer(ETHERNET_SCORE, specificCaps, {r -> r.run()}, specificOfferCb)
+ // bring up reserved reservation offer
+ val reservedOfferCaps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+ val reservedOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ provider.registerNetworkOffer(ETHERNET_SCORE, reservedOfferCaps, reservedOfferCb)
// validate onReserved was sent to the app
- val reservedCaps = cb.expect<Reserved>().caps
- assertEquals(specificCaps, reservedCaps)
+ val onReservedCaps = cb.expect<Reserved>().caps
+ assertEquals(reservedOfferCaps, onReservedCaps)
- // validate the reservation matches the specific offer.
- specificOfferCb.expectOnNetworkNeeded(specificCaps)
+ // validate the reservation matches the reserved offer.
+ reservedOfferCb.expectOnNetworkNeeded(reservedOfferCaps)
- // Specific offer goes away
- provider.unregisterNetworkOffer(specificOfferCb)
+ // reserved offer goes away
+ provider.unregisterNetworkOffer(reservedOfferCb)
cb.expect<Unavailable>()
}
@@ -101,19 +126,168 @@
@Test
fun testReservationRequest_notDeliveredToRegularOffer() {
- val provider = NetworkProvider(context, csHandlerThread.looper, "Ethernet provider")
val offerCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
-
- cm.registerNetworkProvider(provider)
provider.registerNetworkOffer(ETHERNET_SCORE, ETHERNET_CAPS, {r -> r.run()}, offerCb)
- val req = NetworkRequest.Builder().addTransportType(TRANSPORT_ETHERNET).build()
val cb = TestableNetworkCallback()
- cm.reserveNetwork(req, csHandler, cb)
+ cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
// validate the offer does not receive onNetworkNeeded for reservation request
offerCb.expectNoCallbackWhere {
it is OnNetworkNeeded && it.request.type == NetworkRequest.Type.RESERVATION
}
}
+
+ @Test
+ fun testReservedOffer_preventReservationIdUpdate() {
+ val cb = TestableNetworkCallback()
+ cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+
+ // validate the reservation matches the blanket offer.
+ val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+ val reservationId = reservationReq.networkCapabilities.reservationId
+
+ // bring up reserved offer
+ val reservedCaps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+ val reservedOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ provider.registerNetworkOffer(ETHERNET_SCORE, reservedCaps, reservedOfferCb)
+
+ cb.expect<Reserved>()
+ reservedOfferCb.expectOnNetworkNeeded(reservedCaps)
+
+ // try to update the offer's reservationId by reusing the same callback object.
+ // first file a new request to try and match the offer later.
+ val cb2 = TestableNetworkCallback()
+ cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb2)
+
+ val reservationReq2 = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+ val reservationId2 = reservationReq2.networkCapabilities.reservationId
+
+ // try to update the offer's reservationId to an existing reservationId.
+ val updatedCaps = ETHERNET_CAPS.copyWithReservationId(reservationId2)
+ provider.registerNetworkOffer(ETHERNET_SCORE, updatedCaps, reservedOfferCb)
+
+ // validate the original offer disappeared.
+ cb.expect<Unavailable>()
+ // validate the new offer was rejected by CS.
+ reservedOfferCb.expectOnNetworkUnneeded(reservedCaps)
+ // validate cb2 never sees onReserved().
+ cb2.assertNoCallback()
+ }
+
+ @Test
+ fun testReservedOffer_capabilitiesCannotBeUpdated() {
+ val cb = TestableNetworkCallback()
+ cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+
+ val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+ val reservationId = reservationReq.networkCapabilities.reservationId
+
+ val reservedCaps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+ val reservedOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ provider.registerNetworkOffer(ETHERNET_SCORE, reservedCaps, reservedOfferCb)
+
+ cb.expect<Reserved>()
+ reservedOfferCb.expectOnNetworkNeeded(reservedCaps)
+
+ // update reserved offer capabilities
+ val updatedCaps = NetworkCapabilities(reservedCaps).addCapability(NET_CAPABILITY_WIFI_P2P)
+ provider.registerNetworkOffer(ETHERNET_SCORE, updatedCaps, reservedOfferCb)
+
+ cb.expect<Unavailable>()
+ reservedOfferCb.expectOnNetworkUnneeded(reservedCaps)
+ reservedOfferCb.assertNoCallback()
+ }
+
+ @Test
+ fun testBlanketOffer_updateAllowed() {
+ val cb = TestableNetworkCallback()
+ cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+ blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS)
+
+ val updatedCaps = NetworkCapabilities(BLANKET_CAPS).addCapability(NET_CAPABILITY_WIFI_P2P)
+ provider.registerNetworkOffer(ETHERNET_SCORE, updatedCaps, blanketOffer)
+ blanketOffer.assertNoCallback()
+
+ // Note: NetworkRequest.Builder(NetworkRequest) *does not* perform a defensive copy but
+ // changes the underlying request.
+ val p2pRequest = NetworkRequest.Builder(NetworkRequest(ETHERNET_REQUEST))
+ .addCapability(NET_CAPABILITY_WIFI_P2P)
+ .build()
+ cm.reserveNetwork(p2pRequest, csHandler, cb)
+ blanketOffer.expectOnNetworkNeeded(updatedCaps)
+ }
+
+ @Test
+ fun testReservationOffer_onlyAllowSingleOffer() {
+ val cb = TestableNetworkCallback()
+ cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+
+ val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+ val reservationId = reservationReq.networkCapabilities.reservationId
+
+ val offerCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ val caps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+ provider.registerNetworkOffer(ETHERNET_SCORE, caps, offerCb)
+ offerCb.expectOnNetworkNeeded(caps)
+ cb.expect<Reserved>()
+
+ val newOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ provider.registerNetworkOffer(ETHERNET_SCORE, caps, newOfferCb)
+ newOfferCb.assertNoCallback()
+ cb.assertNoCallback()
+
+ // File a regular request and validate only the old offer gets onNetworkNeeded.
+ val cb2 = TestableNetworkCallback()
+ cm.requestNetwork(ETHERNET_REQUEST, cb2, csHandler)
+ offerCb.expectOnNetworkNeeded(caps)
+ newOfferCb.assertNoCallback()
+ }
+
+ @Test
+ fun testReservationOffer_updateScore() {
+ val cb = TestableNetworkCallback()
+ cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+
+ val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+ val reservationId = reservationReq.networkCapabilities.reservationId
+
+ val reservedCaps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+ val reservedOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ provider.registerNetworkOffer(ETHERNET_SCORE, reservedCaps, reservedOfferCb)
+ reservedOfferCb.expectOnNetworkNeeded(reservedCaps)
+ reservedOfferCb.assertNoCallback()
+ cb.expect<Reserved>()
+
+ // update reserved offer capabilities
+ val newScore = NetworkScore.Builder().setShouldYieldToBadWifi(true).build()
+ provider.registerNetworkOffer(newScore, reservedCaps, reservedOfferCb)
+ cb.assertNoCallback()
+
+ val cb2 = TestableNetworkCallback()
+ cm.requestNetwork(ETHERNET_REQUEST, cb2, csHandler)
+ reservedOfferCb.expectOnNetworkNeeded(reservedCaps)
+ reservedOfferCb.assertNoCallback()
+ }
+
+ @Test
+ fun testReservationOffer_regularOfferCanBeUpdated() {
+ val offerCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ provider.registerNetworkOffer(ETHERNET_SCORE, ETHERNET_CAPS, offerCb)
+
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(ETHERNET_REQUEST, cb, csHandler)
+ offerCb.expectOnNetworkNeeded(ETHERNET_CAPS)
+ offerCb.assertNoCallback()
+
+ val updatedCaps = NetworkCapabilities(ETHERNET_CAPS).addCapability(NET_CAPABILITY_WIFI_P2P)
+ val newScore = NetworkScore.Builder().setShouldYieldToBadWifi(true).build()
+ provider.registerNetworkOffer(newScore, updatedCaps, offerCb)
+ offerCb.assertNoCallback()
+
+ val cb2 = TestableNetworkCallback()
+ cm.requestNetwork(ETHERNET_REQUEST, cb2, csHandler)
+ offerCb.expectOnNetworkNeeded(ETHERNET_CAPS)
+ offerCb.assertNoCallback()
+ }
}
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
index e6aba22..533bbf8 100644
--- a/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
+++ b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
@@ -79,6 +79,7 @@
initMockResources();
doReturn(false).when(mFactory).updateInterfaceLinkState(anyString(), anyBoolean());
doReturn(new String[0]).when(mNetd).interfaceGetList();
+ doReturn(new String[0]).when(mFactory).getAvailableInterfaces(anyBoolean());
mHandlerThread = new HandlerThread(THREAD_NAME);
mHandlerThread.start();
tracker = new EthernetTracker(mContext, mHandlerThread.getThreadHandler(), mFactory, mNetd,
@@ -166,9 +167,10 @@
EthernetTracker.parseStaticIpConfiguration(configAsString));
}
- private NetworkCapabilities.Builder makeEthernetCapabilitiesBuilder(boolean clearAll) {
+ private NetworkCapabilities.Builder makeEthernetCapabilitiesBuilder(boolean clearDefaults) {
final NetworkCapabilities.Builder builder =
- clearAll ? NetworkCapabilities.Builder.withoutDefaultCapabilities()
+ clearDefaults
+ ? NetworkCapabilities.Builder.withoutDefaultCapabilities()
: new NetworkCapabilities.Builder();
return builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED)
@@ -176,21 +178,20 @@
}
/**
- * Test: Attempt to create a capabilties with various valid sets of capabilities/transports
+ * Test: Attempt to create a capabilities with various valid sets of capabilities/transports
*/
@Test
public void createNetworkCapabilities() {
-
// Particularly common expected results
- NetworkCapabilities defaultEthernetCleared =
- makeEthernetCapabilitiesBuilder(true /* clearAll */)
+ NetworkCapabilities defaultCapabilities =
+ makeEthernetCapabilitiesBuilder(false /* clearDefaults */)
.setLinkUpstreamBandwidthKbps(100000)
.setLinkDownstreamBandwidthKbps(100000)
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
.build();
NetworkCapabilities ethernetClearedWithCommonCaps =
- makeEthernetCapabilitiesBuilder(true /* clearAll */)
+ makeEthernetCapabilitiesBuilder(true /* clearDefaults */)
.setLinkUpstreamBandwidthKbps(100000)
.setLinkDownstreamBandwidthKbps(100000)
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
@@ -200,89 +201,71 @@
.addCapability(15)
.build();
- // Empty capabilities and transports lists with a "please clear defaults" should
- // yield an empty capabilities set with TRANPORT_ETHERNET
- assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "");
+ // Empty capabilities and transports should return the default capabilities set
+ // with TRANSPORT_ETHERNET
+ assertParsedNetworkCapabilities(defaultCapabilities, "", "");
- // Empty capabilities and transports without the clear defaults flag should return the
- // default capabilities set with TRANSPORT_ETHERNET
- assertParsedNetworkCapabilities(
- makeEthernetCapabilitiesBuilder(false /* clearAll */)
- .setLinkUpstreamBandwidthKbps(100000)
- .setLinkDownstreamBandwidthKbps(100000)
- .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
- .build(),
- false, "", "");
-
- // A list of capabilities without the clear defaults flag should return the default
- // capabilities, mixed with the desired capabilities, and TRANSPORT_ETHERNET
- assertParsedNetworkCapabilities(
- makeEthernetCapabilitiesBuilder(false /* clearAll */)
- .setLinkUpstreamBandwidthKbps(100000)
- .setLinkDownstreamBandwidthKbps(100000)
- .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
- .addCapability(11)
- .addCapability(12)
- .build(),
- false, "11,12", "");
-
- // Adding a list of capabilities with a clear defaults will leave exactly those capabilities
- // with a default TRANSPORT_ETHERNET since no overrides are specified
- assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "12,13,14,15", "");
+ // Adding a list of capabilities will leave exactly those capabilities with a default
+ // TRANSPORT_ETHERNET since no overrides are specified
+ assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, "12,13,14,15", "");
// Adding any invalid capabilities to the list will cause them to be ignored
- assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "12,13,14,15,65,73", "");
- assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "12,13,14,15,abcdefg", "");
+ assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, "12,13,14,15,65,73", "");
+ assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, "12,13,14,15,abcdefg", "");
// Adding a valid override transport will remove the default TRANSPORT_ETHERNET transport
- // and apply only the override to the capabiltities object
+ // and apply only the override to the capabilities object
assertParsedNetworkCapabilities(
- makeEthernetCapabilitiesBuilder(true /* clearAll */)
+ makeEthernetCapabilitiesBuilder(false /* clearDefaults */)
.setLinkUpstreamBandwidthKbps(100000)
.setLinkDownstreamBandwidthKbps(100000)
.addTransportType(0)
.build(),
- true, "", "0");
+ "",
+ "0");
assertParsedNetworkCapabilities(
- makeEthernetCapabilitiesBuilder(true /* clearAll */)
+ makeEthernetCapabilitiesBuilder(false /* clearDefaults */)
.setLinkUpstreamBandwidthKbps(100000)
.setLinkDownstreamBandwidthKbps(100000)
.addTransportType(1)
.build(),
- true, "", "1");
+ "",
+ "1");
assertParsedNetworkCapabilities(
- makeEthernetCapabilitiesBuilder(true /* clearAll */)
+ makeEthernetCapabilitiesBuilder(false /* clearDefaults */)
.setLinkUpstreamBandwidthKbps(100000)
.setLinkDownstreamBandwidthKbps(100000)
.addTransportType(2)
.build(),
- true, "", "2");
+ "",
+ "2");
assertParsedNetworkCapabilities(
- makeEthernetCapabilitiesBuilder(true /* clearAll */)
+ makeEthernetCapabilitiesBuilder(false /* clearDefaults */)
.setLinkUpstreamBandwidthKbps(100000)
.setLinkDownstreamBandwidthKbps(100000)
.addTransportType(3)
.build(),
- true, "", "3");
+ "",
+ "3");
- // "4" is TRANSPORT_VPN, which is unsupported. Should default back to TRANPORT_ETHERNET
- assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "4");
+ // "4" is TRANSPORT_VPN, which is unsupported. Should default back to TRANSPORT_ETHERNET
+ assertParsedNetworkCapabilities(defaultCapabilities, "", "4");
// "5" is TRANSPORT_WIFI_AWARE, which is currently supported due to no legacy TYPE_NONE
// conversion. When that becomes available, this test must be updated
- assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "5");
+ assertParsedNetworkCapabilities(defaultCapabilities, "", "5");
// "6" is TRANSPORT_LOWPAN, which is currently supported due to no legacy TYPE_NONE
// conversion. When that becomes available, this test must be updated
- assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "6");
+ assertParsedNetworkCapabilities(defaultCapabilities, "", "6");
// Adding an invalid override transport will leave the transport as TRANSPORT_ETHERNET
- assertParsedNetworkCapabilities(defaultEthernetCleared,true, "", "100");
- assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "abcdefg");
+ assertParsedNetworkCapabilities(defaultCapabilities, "", "100");
+ assertParsedNetworkCapabilities(defaultCapabilities, "", "abcdefg");
// Ensure the adding of both capabilities and transports work
assertParsedNetworkCapabilities(
- makeEthernetCapabilitiesBuilder(true /* clearAll */)
+ makeEthernetCapabilitiesBuilder(true /* clearDefaults */)
.setLinkUpstreamBandwidthKbps(100000)
.setLinkDownstreamBandwidthKbps(100000)
.addCapability(12)
@@ -291,17 +274,21 @@
.addCapability(15)
.addTransportType(3)
.build(),
- true, "12,13,14,15", "3");
+ "12,13,14,15",
+ "3");
// Ensure order does not matter for capability list
- assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "13,12,15,14", "");
+ assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, "13,12,15,14", "");
}
- private void assertParsedNetworkCapabilities(NetworkCapabilities expectedNetworkCapabilities,
- boolean clearCapabilties, String configCapabiltiies,String configTransports) {
- assertEquals(expectedNetworkCapabilities,
- EthernetTracker.createNetworkCapabilities(clearCapabilties, configCapabiltiies,
- configTransports).build());
+ private void assertParsedNetworkCapabilities(
+ NetworkCapabilities expectedNetworkCapabilities,
+ String configCapabiltiies,
+ String configTransports) {
+ assertEquals(
+ expectedNetworkCapabilities,
+ EthernetTracker.createNetworkCapabilities(configCapabiltiies, configTransports)
+ .build());
}
@Test
diff --git a/tests/unit/java/com/android/server/net/L2capPacketForwarderTest.kt b/tests/unit/java/com/android/server/net/L2capPacketForwarderTest.kt
new file mode 100644
index 0000000..b3095ee
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/L2capPacketForwarderTest.kt
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net
+
+import android.bluetooth.BluetoothSocket
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.ParcelFileDescriptor
+import android.system.Os
+import android.system.OsConstants.AF_UNIX
+import android.system.OsConstants.SHUT_RD
+import android.system.OsConstants.SHUT_WR
+import android.system.OsConstants.SOCK_SEQPACKET
+import android.system.OsConstants.SOL_SOCKET
+import android.system.OsConstants.SO_RCVTIMEO
+import android.system.OsConstants.SO_SNDTIMEO
+import android.system.StructTimeval
+import com.android.server.net.L2capPacketForwarder.BluetoothSocketWrapper
+import com.android.server.net.L2capPacketForwarder.FdWrapper
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.google.common.truth.Truth.assertThat
+import java.io.FileDescriptor
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
+import java.nio.ByteBuffer
+import kotlin.arrayOf
+import kotlin.random.Random
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+private const val TIMEOUT = 1000L
+
+@ConnectivityModuleTest
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class L2capPacketForwarderTest {
+ private lateinit var forwarder: L2capPacketForwarder
+ private val tunFds = arrayOf(FileDescriptor(), FileDescriptor())
+ private val l2capFds = arrayOf(FileDescriptor(), FileDescriptor())
+ private lateinit var l2capInputStream: BluetoothL2capInputStream
+ private lateinit var l2capOutputStream: BluetoothL2capOutputStream
+ @Mock private lateinit var bluetoothSocket: BluetoothSocket
+ @Mock private lateinit var callback: L2capPacketForwarder.ICallback
+
+ private val handlerThread = HandlerThread("L2capPacketForwarderTest thread").apply { start() }
+ private val handler = Handler(handlerThread.looper)
+
+ /** Imitates the behavior of an L2CAP BluetoothSocket */
+ private class BluetoothL2capInputStream(
+ val fd: FileDescriptor,
+ ) : InputStream() {
+ val l2capBuffer = ByteBuffer.wrap(ByteArray(0xffff)).apply {
+ limit(0)
+ }
+
+ override fun read(): Int {
+ throw NotImplementedError("b/391623333: not implemented correctly for L2cap sockets")
+ }
+
+ /** See BluetoothSocket#read(buf, off, len) */
+ override fun read(b: ByteArray, off: Int, len: Int): Int {
+ // If no more bytes are remaining, read from the fd into the intermediate buffer.
+ if (l2capBuffer.remaining() == 0) {
+ // fillL2capRxBuffer()
+ // refill buffer and return - 1
+ val backingArray = l2capBuffer.array()
+ var bytesRead = 0
+ try {
+ bytesRead = Os.read(fd, backingArray, 0 /*off*/, backingArray.size)
+ } catch (e: Exception) {
+ // read failed, timed out, or was interrupted
+ // InputStream throws IOException
+ throw IOException(e)
+ }
+ l2capBuffer.rewind()
+ l2capBuffer.limit(bytesRead)
+ }
+
+ val bytesToRead = if (len > l2capBuffer.remaining()) l2capBuffer.remaining() else len
+ l2capBuffer.get(b, off, bytesToRead)
+ return bytesToRead
+ }
+
+ override fun available(): Int {
+ throw NotImplementedError("b/391623333: not implemented correctly for L2cap sockets")
+ }
+
+ override fun close() {
+ try {
+ Os.shutdown(fd, SHUT_RD)
+ } catch (e: Exception) {
+ // InputStream throws IOException
+ throw IOException(e)
+ }
+ }
+ }
+
+ /** Imitates the behavior of an L2CAP BluetoothSocket */
+ private class BluetoothL2capOutputStream(
+ val fd: FileDescriptor,
+ ) : OutputStream() {
+
+ override fun write(b: Int) {
+ throw NotImplementedError("This method does not maintain packet boundaries, do not use")
+ }
+
+ /** See BluetoothSocket#write(buf, off, len) */
+ override fun write(b: ByteArray, off: Int, len: Int) {
+ try {
+ Os.write(fd, b, off, len)
+ } catch (e: Exception) {
+ // OutputStream throws IOException
+ throw IOException(e)
+ }
+ }
+
+ override fun close() {
+ try {
+ Os.shutdown(fd, SHUT_WR)
+ } catch (e: Exception) {
+ // OutputStream throws IOException
+ throw IOException(e)
+ }
+ }
+ }
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ Os.socketpair(AF_UNIX, SOCK_SEQPACKET, 0, tunFds[0], tunFds[1])
+ Os.socketpair(AF_UNIX, SOCK_SEQPACKET, 0, l2capFds[0], l2capFds[1])
+
+ // Set socket i/o timeout for test end.
+ Os.setsockoptTimeval(tunFds[1], SOL_SOCKET, SO_RCVTIMEO, StructTimeval.fromMillis(5000))
+ Os.setsockoptTimeval(tunFds[1], SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(5000))
+ Os.setsockoptTimeval(l2capFds[1], SOL_SOCKET, SO_RCVTIMEO, StructTimeval.fromMillis(5000))
+ Os.setsockoptTimeval(l2capFds[1], SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(5000))
+
+ l2capInputStream = BluetoothL2capInputStream(l2capFds[0])
+ l2capOutputStream = BluetoothL2capOutputStream(l2capFds[0])
+ doReturn(l2capInputStream).`when`(bluetoothSocket).getInputStream()
+ doReturn(l2capOutputStream).`when`(bluetoothSocket).getOutputStream()
+ doAnswer({
+ l2capInputStream.close()
+ l2capOutputStream.close()
+ try {
+ // libcore's Linux_close properly invalidates the FileDescriptor, so it is safe to
+ // close multiple times.
+ Os.close(l2capFds[0])
+ } catch (e: Exception) {
+ // BluetoothSocket#close can be called multiple times. Catch EBADF on subsequent
+ // invocations.
+ }
+ }).`when`(bluetoothSocket).close()
+
+ forwarder = L2capPacketForwarder(
+ handler,
+ FdWrapper(ParcelFileDescriptor(tunFds[0])),
+ BluetoothSocketWrapper(bluetoothSocket),
+ callback
+ )
+ }
+
+ @After
+ fun tearDown() {
+ if (::forwarder.isInitialized) {
+ // forwarder closes tunFds[0] and l2capFds[0]
+ forwarder.tearDown()
+ } else {
+ Os.close(tunFds[0])
+ Os.close(l2capFds[0])
+ }
+ Os.close(tunFds[1])
+ Os.close(l2capFds[1])
+
+ handlerThread.quitSafely()
+ handlerThread.join()
+ }
+
+ fun sendPacket(fd: FileDescriptor, size: Int = 1280): ByteArray {
+ val packet = ByteArray(size)
+ Random.nextBytes(packet)
+ Os.write(fd, packet, 0 /*off*/, packet.size)
+ return packet
+ }
+
+ fun assertPacketReceived(fd: FileDescriptor, expected: ByteArray) {
+ val readBuffer = ByteArray(expected.size)
+ Os.read(fd, readBuffer, 0 /*off*/, readBuffer.size)
+ assertThat(readBuffer).isEqualTo(expected)
+ }
+
+ @Test
+ fun testForwarding_withoutHeaderCompression() {
+ var packet = sendPacket(l2capFds[1])
+ var packet2 = sendPacket(l2capFds[1])
+ assertPacketReceived(tunFds[1], packet)
+ assertPacketReceived(tunFds[1], packet2)
+
+ packet = sendPacket(tunFds[1])
+ packet2 = sendPacket(tunFds[1])
+ assertPacketReceived(l2capFds[1], packet)
+ assertPacketReceived(l2capFds[1], packet2)
+ }
+
+ @Test
+ fun testForwarding_packetExceedsMtu() {
+ // Reading from tun drops packets that exceed MTU.
+ // drop
+ sendPacket(tunFds[1], L2capPacketForwarder.MTU + 1)
+ // drop
+ sendPacket(tunFds[1], L2capPacketForwarder.MTU + 42)
+ var packet = sendPacket(l2capFds[1], 1280)
+ assertPacketReceived(tunFds[1], packet)
+
+ // On the BluetoothSocket side, reads that exceed MTU stop forwarding.
+ sendPacket(l2capFds[1], L2capPacketForwarder.MTU + 1)
+ verify(callback, timeout(TIMEOUT)).onError()
+ }
+}
diff --git a/tests/unit/jni/Android.bp b/tests/unit/jni/Android.bp
index 50971e7..1a833e1 100644
--- a/tests/unit/jni/Android.bp
+++ b/tests/unit/jni/Android.bp
@@ -42,7 +42,7 @@
],
static_libs: [
"libnet_utils_device_common_bpfjni",
- "libnet_utils_device_common_timerfdjni",
+ "libserviceconnectivityjni",
"libtcutils",
],
shared_libs: [
diff --git a/tests/unit/jni/android_net_frameworktests_util/onload.cpp b/tests/unit/jni/android_net_frameworktests_util/onload.cpp
index a0ce4f8..f70b04b 100644
--- a/tests/unit/jni/android_net_frameworktests_util/onload.cpp
+++ b/tests/unit/jni/android_net_frameworktests_util/onload.cpp
@@ -24,7 +24,7 @@
int register_com_android_net_module_util_BpfMap(JNIEnv* env, char const* class_name);
int register_com_android_net_module_util_TcUtils(JNIEnv* env, char const* class_name);
-int register_com_android_net_module_util_TimerFdUtils(JNIEnv *env,
+int register_com_android_net_module_util_ServiceConnectivityJni(JNIEnv *env,
char const *class_name);
extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
@@ -40,8 +40,8 @@
if (register_com_android_net_module_util_TcUtils(env,
"android/net/frameworktests/util/TcUtils") < 0) return JNI_ERR;
- if (register_com_android_net_module_util_TimerFdUtils(
- env, "android/net/frameworktests/util/TimerFdUtils") < 0)
+ if (register_com_android_net_module_util_ServiceConnectivityJni(
+ env, "android/net/frameworktests/util/ServiceConnectivityJni") < 0)
return JNI_ERR;
return JNI_VERSION_1_6;
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index c55096b..af16d19 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -623,12 +623,17 @@
mNat64CidrController.maybeUpdateNat64Cidr();
}
- private static OtDaemonConfiguration newOtDaemonConfig(
- @NonNull ThreadConfiguration threadConfig) {
+ private OtDaemonConfiguration newOtDaemonConfig(ThreadConfiguration threadConfig) {
+ int srpServerConfig = R.bool.config_thread_srp_server_wait_for_border_routing_enabled;
+ boolean srpServerWaitEnabled = mResources.get().getBoolean(srpServerConfig);
+ int autoJoinConfig = R.bool.config_thread_border_router_auto_join_enabled;
+ boolean autoJoinEnabled = mResources.get().getBoolean(autoJoinConfig);
return new OtDaemonConfiguration.Builder()
.setBorderRouterEnabled(threadConfig.isBorderRouterEnabled())
.setNat64Enabled(threadConfig.isNat64Enabled())
.setDhcpv6PdEnabled(threadConfig.isDhcpv6PdEnabled())
+ .setSrpServerWaitForBorderRoutingEnabled(srpServerWaitEnabled)
+ .setBorderRouterAutoJoinEnabled(autoJoinEnabled)
.build();
}
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 316f570..801e21e 100644
--- a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
+++ b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
@@ -39,6 +39,7 @@
import android.os.SystemClock
import android.system.OsConstants
import android.system.OsConstants.IPPROTO_ICMP
+import android.util.Log
import androidx.test.core.app.ApplicationProvider
import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
import com.android.net.module.util.IpUtils
@@ -84,6 +85,8 @@
/** Utilities for Thread integration tests. */
object IntegrationTestUtils {
+ private val TAG = IntegrationTestUtils::class.simpleName
+
// The timeout of join() after restarting ot-daemon. The device needs to send 6 Link Request
// every 5 seconds, followed by 4 Parent Request every second. So this value needs to be 40
// seconds to be safe
@@ -483,6 +486,7 @@
val serviceInfoFuture = CompletableFuture<NsdServiceInfo>()
val listener: NsdManager.DiscoveryListener = object : DefaultDiscoveryListener() {
override fun onServiceFound(serviceInfo: NsdServiceInfo) {
+ Log.d(TAG, "onServiceFound: $serviceInfo")
serviceInfoFuture.complete(serviceInfo)
}
}
@@ -530,6 +534,7 @@
val resolvedServiceInfoFuture = CompletableFuture<NsdServiceInfo>()
val callback: NsdManager.ServiceInfoCallback = object : DefaultServiceInfoCallback() {
override fun onServiceUpdated(serviceInfo: NsdServiceInfo) {
+ Log.d(TAG, "onServiceUpdated: $serviceInfo")
if (predicate.test(serviceInfo)) {
resolvedServiceInfoFuture.complete(serviceInfo)
}
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
index dcbb3f5..bc8da8b 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -231,6 +231,11 @@
when(mConnectivityResources.get()).thenReturn(mResources);
when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(true);
+ when(mResources.getBoolean(
+ eq(R.bool.config_thread_srp_server_wait_for_border_routing_enabled)))
+ .thenReturn(true);
+ when(mResources.getBoolean(eq(R.bool.config_thread_border_router_auto_join_enabled)))
+ .thenReturn(true);
when(mResources.getString(eq(R.string.config_thread_vendor_name)))
.thenReturn(TEST_VENDOR_NAME);
when(mResources.getString(eq(R.string.config_thread_vendor_oui)))
@@ -285,6 +290,11 @@
@Test
public void initialize_resourceOverlayValuesAreSetToOtDaemon() throws Exception {
+ when(mResources.getBoolean(
+ eq(R.bool.config_thread_srp_server_wait_for_border_routing_enabled)))
+ .thenReturn(false);
+ when(mResources.getBoolean(eq(R.bool.config_thread_border_router_auto_join_enabled)))
+ .thenReturn(false);
when(mResources.getString(eq(R.string.config_thread_vendor_name)))
.thenReturn(TEST_VENDOR_NAME);
when(mResources.getString(eq(R.string.config_thread_vendor_oui)))
@@ -297,6 +307,8 @@
mService.initialize();
mTestLooper.dispatchAll();
+ assertThat(mFakeOtDaemon.getConfiguration().srpServerWaitForBorderRoutingEnabled).isFalse();
+ assertThat(mFakeOtDaemon.getConfiguration().borderRouterAutoJoinEnabled).isFalse();
MeshcopTxtAttributes meshcopTxts = mFakeOtDaemon.getOverriddenMeshcopTxtAttributes();
assertThat(meshcopTxts.vendorName).isEqualTo(TEST_VENDOR_NAME);
assertThat(meshcopTxts.vendorOui).isEqualTo(TEST_VENDOR_OUI_BYTES);