Merge "Revert^2 "Re-enable the tests for background restrictions"" into main
diff --git a/common/FlaggedApi.bp b/common/FlaggedApi.bp
index c382e76..449d7ae 100644
--- a/common/FlaggedApi.bp
+++ b/common/FlaggedApi.bp
@@ -21,3 +21,11 @@
srcs: ["flags.aconfig"],
visibility: ["//packages/modules/Connectivity:__subpackages__"],
}
+
+aconfig_declarations {
+ name: "nearby_flags",
+ package: "com.android.nearby.flags",
+ container: "system",
+ srcs: ["nearby_flags.aconfig"],
+ visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
diff --git a/common/nearby_flags.aconfig b/common/nearby_flags.aconfig
new file mode 100644
index 0000000..b957d33
--- /dev/null
+++ b/common/nearby_flags.aconfig
@@ -0,0 +1,9 @@
+package: "com.android.nearby.flags"
+container: "system"
+
+flag {
+ name: "powered_off_finding"
+ namespace: "nearby"
+ description: "Controls whether the Powered Off Finding feature is enabled"
+ bug: "307898240"
+}
diff --git a/framework-t/Android.bp b/framework-t/Android.bp
index 9203a3e..e40b55c 100644
--- a/framework-t/Android.bp
+++ b/framework-t/Android.bp
@@ -197,6 +197,7 @@
],
aconfig_declarations: [
"com.android.net.flags-aconfig",
+ "nearby_flags",
],
}
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 1ea1815..915ec52 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -74,6 +74,7 @@
import android.util.SparseIntArray;
import com.android.internal.annotations.GuardedBy;
+import com.android.modules.utils.build.SdkLevel;
import libcore.net.event.NetworkEventDispatcher;
@@ -6278,9 +6279,13 @@
// Only the system server process and the network stack have access.
@FlaggedApi(Flags.SUPPORT_IS_UID_NETWORKING_BLOCKED)
@SystemApi(client = MODULE_LIBRARIES)
- @RequiresApi(Build.VERSION_CODES.TIRAMISU) // BPF maps were only mainlined in T
+ // Note b/326143935 kernel bug can trigger crash on some T device.
+ @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
@RequiresPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
public boolean isUidNetworkingBlocked(int uid, boolean isNetworkMetered) {
+ if (!SdkLevel.isAtLeastU()) {
+ Log.wtf(TAG, "isUidNetworkingBlocked is not supported on pre-U devices");
+ }
final BpfNetMapsReader reader = BpfNetMapsReader.getInstance();
// Note that before V, the data saver status in bpf is written by ConnectivityService
// when receiving {@link #ACTION_RESTRICT_BACKGROUND_CHANGED}. Thus,
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index f6ef75e..84a0d29 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -1770,9 +1770,13 @@
public @NonNull NetworkCapabilities setNetworkSpecifier(
@NonNull NetworkSpecifier networkSpecifier) {
if (networkSpecifier != null
- // Transport can be test, or test + a single other transport
+ // Transport can be test, or test + a single other transport or cellular + satellite
+ // transport. Note: cellular + satellite combination is allowed since both transport
+ // use the same specifier, TelephonyNetworkSpecifier.
&& mTransportTypes != (1L << TRANSPORT_TEST)
- && Long.bitCount(mTransportTypes & ~(1L << TRANSPORT_TEST)) != 1) {
+ && Long.bitCount(mTransportTypes & ~(1L << TRANSPORT_TEST)) != 1
+ && (mTransportTypes & ~(1L << TRANSPORT_TEST))
+ != (1 << TRANSPORT_CELLULAR | 1 << TRANSPORT_SATELLITE)) {
throw new IllegalStateException("Must have a single non-test transport specified to "
+ "use setNetworkSpecifier");
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
index fe9bbba..56202fd 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
@@ -118,6 +118,14 @@
}
/**
+ * Indicates whether {@link #NSD_KNOWN_ANSWER_SUPPRESSION} is enabled, including for testing.
+ */
+ public boolean isKnownAnswerSuppressionEnabled() {
+ return mIsKnownAnswerSuppressionEnabled
+ || isForceEnabledForTest(NSD_KNOWN_ANSWER_SUPPRESSION);
+ }
+
+ /**
* The constructor for {@link MdnsFeatureFlags}.
*/
public MdnsFeatureFlags(boolean isOffloadFeatureEnabled,
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
index 96a59e2..ed0bde2 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -538,7 +538,7 @@
}
private boolean isTruncatedKnownAnswerPacket(MdnsPacket packet) {
- if (!mMdnsFeatureFlags.mIsKnownAnswerSuppressionEnabled
+ if (!mMdnsFeatureFlags.isKnownAnswerSuppressionEnabled()
// Should ignore the response packet.
|| (packet.flags & MdnsConstants.FLAGS_RESPONSE) != 0) {
return false;
@@ -745,7 +745,7 @@
// RR TTL as known by the Multicast DNS responder, the responder MUST
// send an answer so as to update the querier's cache before the record
// becomes in danger of expiration.
- if (mMdnsFeatureFlags.mIsKnownAnswerSuppressionEnabled
+ if (mMdnsFeatureFlags.isKnownAnswerSuppressionEnabled()
&& isKnownAnswer(info.record, knownAnswerRecords)) {
continue;
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java b/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
index a46be3b..db3845a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
@@ -145,7 +145,7 @@
public void queueReply(@NonNull MdnsReplyInfo reply) {
ensureRunningOnHandlerThread(mHandler);
- if (mMdnsFeatureFlags.mIsKnownAnswerSuppressionEnabled) {
+ if (mMdnsFeatureFlags.isKnownAnswerSuppressionEnabled()) {
mDependencies.removeMessages(mHandler, MSG_SEND, reply.source);
final MdnsReplyInfo queuingReply = mSrcReplies.remove(reply.source);
@@ -231,7 +231,7 @@
@Override
public void handleMessage(@NonNull Message msg) {
final MdnsReplyInfo replyInfo;
- if (mMdnsFeatureFlags.mIsKnownAnswerSuppressionEnabled) {
+ if (mMdnsFeatureFlags.isKnownAnswerSuppressionEnabled()) {
// Retrieve the MdnsReplyInfo from the map via a source address, as the reply info
// will be combined or updated.
final InetSocketAddress source = (InetSocketAddress) msg.obj;
diff --git a/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java b/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
new file mode 100644
index 0000000..8598ac4
--- /dev/null
+++ b/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
@@ -0,0 +1,134 @@
+/*
+ * 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;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.NetworkStats;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.time.Clock;
+import java.util.HashMap;
+import java.util.Objects;
+
+/**
+ * A thread-safe cache for storing and retrieving {@link NetworkStats.Entry} objects,
+ * with an adjustable expiry duration to manage data freshness.
+ */
+class TrafficStatsRateLimitCache {
+ private final Clock mClock;
+ private final long mExpiryDurationMs;
+
+ /**
+ * Constructs a new {@link TrafficStatsRateLimitCache} with the specified expiry duration.
+ *
+ * @param clock The {@link Clock} to use for determining timestamps.
+ * @param expiryDurationMs The expiry duration in milliseconds.
+ */
+ TrafficStatsRateLimitCache(@NonNull Clock clock, long expiryDurationMs) {
+ mClock = clock;
+ mExpiryDurationMs = expiryDurationMs;
+ }
+
+ private static class TrafficStatsCacheKey {
+ @Nullable
+ public final String iface;
+ public final int uid;
+
+ TrafficStatsCacheKey(@Nullable String iface, int uid) {
+ this.iface = iface;
+ this.uid = uid;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof TrafficStatsCacheKey)) return false;
+ TrafficStatsCacheKey that = (TrafficStatsCacheKey) o;
+ return uid == that.uid && Objects.equals(iface, that.iface);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(iface, uid);
+ }
+ }
+
+ private static class TrafficStatsCacheValue {
+ public final long timestamp;
+ @NonNull
+ public final NetworkStats.Entry entry;
+
+ TrafficStatsCacheValue(long timestamp, NetworkStats.Entry entry) {
+ this.timestamp = timestamp;
+ this.entry = entry;
+ }
+ }
+
+ @GuardedBy("mMap")
+ private final HashMap<TrafficStatsCacheKey, TrafficStatsCacheValue> mMap = new HashMap<>();
+
+ /**
+ * Retrieves a {@link NetworkStats.Entry} from the cache, associated with the given key.
+ *
+ * @param iface The interface name to include in the cache key. Null if not applicable.
+ * @param uid The UID to include in the cache key. {@code UID_ALL} if not applicable.
+ * @return The cached {@link NetworkStats.Entry}, or null if not found or expired.
+ */
+ @Nullable
+ NetworkStats.Entry get(String iface, int uid) {
+ final TrafficStatsCacheKey key = new TrafficStatsCacheKey(iface, uid);
+ synchronized (mMap) { // Synchronize for thread-safety
+ final TrafficStatsCacheValue value = mMap.get(key);
+ if (value != null && !isExpired(value.timestamp)) {
+ return value.entry;
+ } else {
+ mMap.remove(key); // Remove expired entries
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Stores a {@link NetworkStats.Entry} in the cache, associated with the given key.
+ *
+ * @param iface The interface name to include in the cache key. Null if not applicable.
+ * @param uid The UID to include in the cache key. {@code UID_ALL} if not applicable.
+ * @param entry The {@link NetworkStats.Entry} to store in the cache.
+ */
+ void put(String iface, int uid, @NonNull final NetworkStats.Entry entry) {
+ Objects.requireNonNull(entry);
+ final TrafficStatsCacheKey key = new TrafficStatsCacheKey(iface, uid);
+ synchronized (mMap) { // Synchronize for thread-safety
+ mMap.put(key, new TrafficStatsCacheValue(mClock.millis(), entry));
+ }
+ }
+
+ /**
+ * Clear the cache.
+ */
+ void clear() {
+ synchronized (mMap) {
+ mMap.clear();
+ }
+ }
+
+ private boolean isExpired(long timestamp) {
+ return mClock.millis() > timestamp + mExpiryDurationMs;
+ }
+}
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 3d646fd..6839c22 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -6009,7 +6009,7 @@
if (nm == null) return;
if (request == CaptivePortal.APP_REQUEST_REEVALUATION_REQUIRED) {
- hasNetworkStackPermission();
+ enforceNetworkStackPermission(mContext);
nm.forceReevaluation(mDeps.getCallingUid());
}
}
diff --git a/service/src/com/android/server/connectivity/SatelliteAccessController.java b/service/src/com/android/server/connectivity/SatelliteAccessController.java
index 0968aff..b53abce 100644
--- a/service/src/com/android/server/connectivity/SatelliteAccessController.java
+++ b/service/src/com/android/server/connectivity/SatelliteAccessController.java
@@ -26,8 +26,10 @@
import android.os.Handler;
import android.os.Process;
import android.os.UserHandle;
+import android.os.UserManager;
import android.util.ArraySet;
import android.util.Log;
+import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
@@ -44,13 +46,18 @@
*/
public class SatelliteAccessController {
private static final String TAG = SatelliteAccessController.class.getSimpleName();
- private final PackageManager mPackageManager;
+ private final Context mContext;
private final Dependencies mDeps;
private final DefaultMessageRoleListener mDefaultMessageRoleListener;
+ private final UserManager mUserManager;
private final Consumer<Set<Integer>> mCallback;
- private final Set<Integer> mSatelliteNetworkPreferredUidCache = new ArraySet<>();
private final Handler mConnectivityServiceHandler;
+ // At this sparseArray, Key is userId and values are uids of SMS apps that are allowed
+ // to use satellite network as fallback.
+ private final SparseArray<Set<Integer>> mAllUsersSatelliteNetworkFallbackUidCache =
+ new SparseArray<>();
+
/**
* Monitor {@link android.app.role.OnRoleHoldersChangedListener#onRoleHoldersChanged(String,
* UserHandle)},
@@ -59,10 +66,10 @@
private final class DefaultMessageRoleListener
implements OnRoleHoldersChangedListener {
@Override
- public void onRoleHoldersChanged(String role, UserHandle user) {
+ public void onRoleHoldersChanged(String role, UserHandle userHandle) {
if (RoleManager.ROLE_SMS.equals(role)) {
Log.i(TAG, "ROLE_SMS Change detected ");
- onRoleSmsChanged();
+ onRoleSmsChanged(userHandle);
}
}
@@ -71,7 +78,7 @@
mDeps.addOnRoleHoldersChangedListenerAsUser(
mConnectivityServiceHandler::post, this, UserHandle.ALL);
} catch (RuntimeException e) {
- Log.e(TAG, "Could not register satellite controller listener due to " + e);
+ Log.wtf(TAG, "Could not register satellite controller listener due to " + e);
}
}
}
@@ -89,9 +96,9 @@
mRoleManager = context.getSystemService(RoleManager.class);
}
- /** See {@link RoleManager#getRoleHolders(String)} */
- public List<String> getRoleHolders(String roleName) {
- return mRoleManager.getRoleHolders(roleName);
+ /** See {@link RoleManager#getRoleHoldersAsUser(String, UserHandle)} */
+ public List<String> getRoleHoldersAsUser(String roleName, UserHandle userHandle) {
+ return mRoleManager.getRoleHoldersAsUser(roleName, userHandle);
}
/** See {@link RoleManager#addOnRoleHoldersChangedListenerAsUser} */
@@ -105,81 +112,107 @@
SatelliteAccessController(@NonNull final Context c, @NonNull final Dependencies deps,
Consumer<Set<Integer>> callback,
@NonNull final Handler connectivityServiceInternalHandler) {
+ mContext = c;
mDeps = deps;
- mPackageManager = c.getPackageManager();
+ mUserManager = mContext.getSystemService(UserManager.class);
mDefaultMessageRoleListener = new DefaultMessageRoleListener();
mCallback = callback;
mConnectivityServiceHandler = connectivityServiceInternalHandler;
}
- private void updateSatelliteNetworkPreferredUidListCache(List<String> packageNames) {
- for (String packageName : packageNames) {
- // Check if SATELLITE_COMMUNICATION permission is enabled for default sms application
- // package before adding it part of satellite network preferred uid cache list.
- if (isSatellitePermissionEnabled(packageName)) {
- mSatelliteNetworkPreferredUidCache.add(getUidForPackage(packageName));
+ private Set<Integer> updateSatelliteNetworkFallbackUidListCache(List<String> packageNames,
+ @NonNull UserHandle userHandle) {
+ Set<Integer> fallbackUids = new ArraySet<>();
+ PackageManager pm =
+ mContext.createContextAsUser(userHandle, 0).getPackageManager();
+ if (pm != null) {
+ for (String packageName : packageNames) {
+ // Check if SATELLITE_COMMUNICATION permission is enabled for default sms
+ // application package before adding it part of satellite network fallback uid
+ // cache list.
+ if (isSatellitePermissionEnabled(pm, packageName)) {
+ int uid = getUidForPackage(pm, packageName);
+ if (uid != Process.INVALID_UID) {
+ fallbackUids.add(uid);
+ }
+ }
}
+ } else {
+ Log.wtf(TAG, "package manager found null");
}
+ return fallbackUids;
}
//Check if satellite communication is enabled for the package
- private boolean isSatellitePermissionEnabled(String packageName) {
- if (mPackageManager != null) {
- return mPackageManager.checkPermission(
- Manifest.permission.SATELLITE_COMMUNICATION, packageName)
- == PackageManager.PERMISSION_GRANTED;
- }
- return false;
+ private boolean isSatellitePermissionEnabled(PackageManager packageManager,
+ String packageName) {
+ return packageManager.checkPermission(
+ Manifest.permission.SATELLITE_COMMUNICATION, packageName)
+ == PackageManager.PERMISSION_GRANTED;
}
- private int getUidForPackage(String pkgName) {
+ private int getUidForPackage(PackageManager packageManager, String pkgName) {
if (pkgName == null) {
return Process.INVALID_UID;
}
try {
- if (mPackageManager != null) {
- ApplicationInfo applicationInfo = mPackageManager.getApplicationInfo(pkgName, 0);
- if (applicationInfo != null) {
- return applicationInfo.uid;
- }
- }
+ ApplicationInfo applicationInfo = packageManager.getApplicationInfo(pkgName, 0);
+ return applicationInfo.uid;
} catch (PackageManager.NameNotFoundException exception) {
Log.e(TAG, "Unable to find uid for package: " + pkgName);
}
return Process.INVALID_UID;
}
- //on Role sms change triggered by OnRoleHoldersChangedListener()
- private void onRoleSmsChanged() {
- final List<String> packageNames = getRoleSmsChangedPackageName();
-
- // Create a new Set
- Set<Integer> previousSatellitePreferredUid = new ArraySet<>(
- mSatelliteNetworkPreferredUidCache);
-
- mSatelliteNetworkPreferredUidCache.clear();
-
- if (packageNames != null) {
- Log.i(TAG, "role_sms_packages: " + packageNames);
- // On Role change listener, update the satellite network preferred uid cache list
- updateSatelliteNetworkPreferredUidListCache(packageNames);
- Log.i(TAG, "satellite_preferred_uid: " + mSatelliteNetworkPreferredUidCache);
- } else {
- Log.wtf(TAG, "package name was found null");
+ // on Role sms change triggered by OnRoleHoldersChangedListener()
+ // TODO(b/326373613): using UserLifecycleListener, callback to be received when user removed for
+ // user delete scenario. This to be used to update uid list and ML Layer request can also be
+ // updated.
+ private void onRoleSmsChanged(@NonNull UserHandle userHandle) {
+ int userId = userHandle.getIdentifier();
+ if (userId == Process.INVALID_UID) {
+ Log.wtf(TAG, "Invalid User Id");
+ return;
}
+ //Returns empty list if no package exists
+ final List<String> packageNames =
+ mDeps.getRoleHoldersAsUser(RoleManager.ROLE_SMS, userHandle);
+
+ // Store previous satellite fallback uid available
+ final Set<Integer> prevUidsForUser =
+ mAllUsersSatelliteNetworkFallbackUidCache.get(userId, new ArraySet<>());
+
+ Log.i(TAG, "currentUser : role_sms_packages: " + userId + " : " + packageNames);
+ final Set<Integer> newUidsForUser = !packageNames.isEmpty()
+ ? updateSatelliteNetworkFallbackUidListCache(packageNames, userHandle)
+ : new ArraySet<>();
+ Log.i(TAG, "satellite_fallback_uid: " + newUidsForUser);
+
// on Role change, update the multilayer request at ConnectivityService with updated
- // satellite network preferred uid cache list if changed or to revoke for previous default
- // sms app
- if (!mSatelliteNetworkPreferredUidCache.equals(previousSatellitePreferredUid)) {
- Log.i(TAG, "update multi layer request");
- mCallback.accept(mSatelliteNetworkPreferredUidCache);
+ // satellite network fallback uid cache list of multiple users as applicable
+ if (newUidsForUser.equals(prevUidsForUser)) {
+ return;
}
+
+ mAllUsersSatelliteNetworkFallbackUidCache.put(userId, newUidsForUser);
+
+ // Merge all uids of multiple users available
+ Set<Integer> mergedSatelliteNetworkFallbackUidCache = new ArraySet<>();
+ for (int i = 0; i < mAllUsersSatelliteNetworkFallbackUidCache.size(); i++) {
+ mergedSatelliteNetworkFallbackUidCache.addAll(
+ mAllUsersSatelliteNetworkFallbackUidCache.valueAt(i));
+ }
+ Log.i(TAG, "merged uid list for multi layer request : "
+ + mergedSatelliteNetworkFallbackUidCache);
+
+ // trigger multiple layer request for satellite network fallback of multi user uids
+ mCallback.accept(mergedSatelliteNetworkFallbackUidCache);
}
- private List<String> getRoleSmsChangedPackageName() {
+ private List<String> getRoleSmsChangedPackageName(UserHandle userHandle) {
try {
- return mDeps.getRoleHolders(RoleManager.ROLE_SMS);
+ return mDeps.getRoleHoldersAsUser(RoleManager.ROLE_SMS, userHandle);
} catch (RuntimeException e) {
Log.wtf(TAG, "Could not get package name at role sms change update due to: " + e);
return null;
@@ -188,7 +221,16 @@
/** Register OnRoleHoldersChangedListener */
public void start() {
- mConnectivityServiceHandler.post(this::onRoleSmsChanged);
+ mConnectivityServiceHandler.post(this::updateAllUserRoleSmsUids);
mDefaultMessageRoleListener.register();
}
+
+ private void updateAllUserRoleSmsUids() {
+ List<UserHandle> existingUsers = mUserManager.getUserHandles(true /* excludeDying */);
+ // Iterate through the user handles and obtain their uids with role sms and satellite
+ // communication permission
+ for (UserHandle userHandle : existingUsers) {
+ onRoleSmsChanged(userHandle);
+ }
+ }
}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ExternalPacketForwarder.kt b/staticlibs/testutils/devicetests/com/android/testutils/ExternalPacketForwarder.kt
new file mode 100644
index 0000000..36eb795
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ExternalPacketForwarder.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.testutils
+
+import java.io.FileDescriptor
+
+class ExternalPacketForwarder(
+ srcFd: FileDescriptor,
+ mtu: Int,
+ dstFd: FileDescriptor,
+ forwardMap: Map<Int, Int>
+) : PacketForwarderBase(srcFd, mtu, dstFd, forwardMap) {
+
+ /**
+ * Prepares a packet for forwarding by potentially updating the
+ * source port based on the specified port remapping rules.
+ *
+ * @param buf The packet data as a byte array.
+ * @param version The IP version of the packet (e.g., 4 for IPv4).
+ */
+ override fun remapPort(buf: ByteArray, version: Int) {
+ val transportOffset = getTransportOffset(version)
+ val intPort = getRemappedPort(buf, transportOffset)
+
+ // Copy remapped source port.
+ if (intPort != 0) {
+ setPortAt(intPort, buf, transportOffset)
+ }
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/InternalPacketForwarder.kt b/staticlibs/testutils/devicetests/com/android/testutils/InternalPacketForwarder.kt
new file mode 100644
index 0000000..58829dc
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/InternalPacketForwarder.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.testutils
+
+import java.io.FileDescriptor
+
+class InternalPacketForwarder(
+ srcFd: FileDescriptor,
+ mtu: Int,
+ dstFd: FileDescriptor,
+ forwardMap: Map<Int, Int>
+) : PacketForwarderBase(srcFd, mtu, dstFd, forwardMap) {
+ /**
+ * Prepares a packet for forwarding by potentially updating the
+ * destination port based on the specified port remapping rules.
+ *
+ * @param buf The packet data as a byte array.
+ * @param version The IP version of the packet (e.g., 4 for IPv4).
+ */
+ override fun remapPort(buf: ByteArray, version: Int) {
+ val transportOffset = getTransportOffset(version) + DESTINATION_PORT_OFFSET
+ val extPort = getRemappedPort(buf, transportOffset)
+
+ // Copy remapped destination port.
+ if (extPort != 0) {
+ setPortAt(extPort, buf, transportOffset)
+ }
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt b/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt
index 1a2cc88..0b736d1 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt
@@ -40,7 +40,8 @@
class PacketBridge(
context: Context,
addresses: List<LinkAddress>,
- dnsAddr: InetAddress
+ dnsAddr: InetAddress,
+ portMapping: List<Pair<Int, Int>>
) {
private val binder = Binder()
@@ -56,6 +57,10 @@
// Register test networks to ConnectivityService.
private val internalNetworkCallback: TestableNetworkCallback
private val externalNetworkCallback: TestableNetworkCallback
+
+ private val internalForwardMap = HashMap<Int, Int>()
+ private val externalForwardMap = HashMap<Int, Int>()
+
val internalNetwork: Network
val externalNetwork: Network
init {
@@ -65,14 +70,28 @@
externalNetworkCallback = exCb
internalNetwork = inNet
externalNetwork = exNet
+ for (mapping in portMapping) {
+ internalForwardMap[mapping.first] = mapping.second
+ externalForwardMap[mapping.second] = mapping.first
+ }
}
// Set up the packet bridge.
private val internalFd = internalIface.fileDescriptor.fileDescriptor
private val externalFd = externalIface.fileDescriptor.fileDescriptor
- private val pr1 = PacketForwarder(internalFd, 1500, externalFd)
- private val pr2 = PacketForwarder(externalFd, 1500, internalFd)
+ private val pr1 = InternalPacketForwarder(
+ internalFd,
+ 1500,
+ externalFd,
+ internalForwardMap
+ )
+ private val pr2 = ExternalPacketForwarder(
+ externalFd,
+ 1500,
+ internalFd,
+ externalForwardMap
+ )
fun start() {
IoUtils.setBlocking(internalFd, true /* blocking */)
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketForwarder.java b/staticlibs/testutils/devicetests/com/android/testutils/PacketForwarderBase.java
similarity index 68%
rename from staticlibs/testutils/devicetests/com/android/testutils/PacketForwarder.java
rename to staticlibs/testutils/devicetests/com/android/testutils/PacketForwarderBase.java
index d8efb7d..5c79eb0 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/PacketForwarder.java
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketForwarderBase.java
@@ -32,6 +32,7 @@
import java.io.FileDescriptor;
import java.io.IOException;
+import java.util.Map;
import java.util.Objects;
/**
@@ -57,8 +58,9 @@
* from the http server, the same mechanism is applied but in a different direction,
* where the source and destination will be swapped.
*/
-public class PacketForwarder extends Thread {
+public abstract class PacketForwarderBase extends Thread {
private static final String TAG = "PacketForwarder";
+ static final int DESTINATION_PORT_OFFSET = 2;
// The source fd to read packets from.
@NonNull
@@ -70,8 +72,10 @@
@NonNull
final FileDescriptor mDstFd;
+ @NonNull
+ final Map<Integer, Integer> mPortRemapRules;
/**
- * Construct a {@link PacketForwarder}.
+ * Construct a {@link PacketForwarderBase}.
*
* This class reads packets from {@code srcFd} of a {@link TestNetworkInterface}, and
* forwards them to the {@code dstFd} of another {@link TestNetworkInterface}.
@@ -82,13 +86,49 @@
* @param srcFd {@link FileDescriptor} to read packets from.
* @param mtu MTU of the test network.
* @param dstFd {@link FileDescriptor} to write packets to.
+ * @param portRemapRules port remap rules
*/
- public PacketForwarder(@NonNull FileDescriptor srcFd, int mtu,
- @NonNull FileDescriptor dstFd) {
+ public PacketForwarderBase(@NonNull FileDescriptor srcFd, int mtu,
+ @NonNull FileDescriptor dstFd,
+ @NonNull Map<Integer, Integer> portRemapRules) {
super(TAG);
mSrcFd = Objects.requireNonNull(srcFd);
mBuf = new byte[mtu];
mDstFd = Objects.requireNonNull(dstFd);
+ mPortRemapRules = Objects.requireNonNull(portRemapRules);
+ }
+
+ /**
+ * A method to prepare forwarding packets between two instances of {@link TestNetworkInterface},
+ * which includes ports mapping.
+ * Subclasses should override this method to implement the needed port remapping.
+ * For internal forwarder will remapped destination port,
+ * external forwarder will remapped source port.
+ * Example:
+ * An outgoing packet from the internal interface with
+ * source 1.2.3.4:1234 and destination 8.8.8.8:80
+ * might be translated to 8.8.8.8:1234 -> 1.2.3.4:8080 before forwarding.
+ * An outgoing packet from the external interface with
+ * source 1.2.3.4:8080 and destination 8.8.8.8:1234
+ * might be translated to 8.8.8.8:80 -> 1.2.3.4:1234 before forwarding.
+ */
+ abstract void remapPort(@NonNull byte[] buf, int version);
+
+ /**
+ * Retrieves a potentially remapped port number from a packet.
+ *
+ * @param buf The packet data as a byte array.
+ * @param transportOffset The offset within the packet where the transport layer port begins.
+ * @return The remapped port if a mapping exists in the internal forwarding map,
+ * otherwise returns 0 (indicating no remapping).
+ */
+ int getRemappedPort(@NonNull byte[] buf, int transportOffset) {
+ int port = PacketReflectorUtil.getPortAt(buf, transportOffset);
+ return mPortRemapRules.getOrDefault(port, 0);
+ }
+
+ int getTransportOffset(int version) {
+ return version == 4 ? IPV4_HEADER_LENGTH : IPV6_HEADER_LENGTH;
}
private void forwardPacket(@NonNull byte[] buf, int len) {
@@ -99,7 +139,13 @@
}
}
- // Reads one packet from mSrcFd, and writes the packet to the mDstFd for supported protocols.
+ /**
+ * Reads one packet from mSrcFd, and writes the packet to the mDestFd for supported protocols.
+ * This includes:
+ * 1.Address Swapping: Swaps source and destination IP addresses.
+ * 2.Port Remapping: Remap port if necessary.
+ * 3.Checksum Recalculation: Updates IP and transport layer checksums to reflect changes.
+ */
private void processPacket() {
final int len = PacketReflectorUtil.readPacket(mSrcFd, mBuf);
if (len < 1) {
@@ -142,13 +188,19 @@
if (len < ipHdrLen + transportHdrLen) {
throw new IllegalStateException("Unexpected buffer length: " + len);
}
- // Swap addresses.
+
+ // Swap source and destination address.
PacketReflectorUtil.swapAddresses(mBuf, version);
+ // Remapping the port.
+ remapPort(mBuf, version);
+
+ // Fix IP and Transport layer checksum.
+ PacketReflectorUtil.fixPacketChecksum(mBuf, len, version, proto);
+
// Send the packet to the destination fd.
forwardPacket(mBuf, len);
}
-
@Override
public void run() {
Log.i(TAG, "starting fd=" + mSrcFd + " valid=" + mSrcFd.valid());
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestHttpServer.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestHttpServer.kt
index 740bf63..f1f0c1c 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestHttpServer.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestHttpServer.kt
@@ -25,8 +25,10 @@
* A minimal HTTP server running on a random available port.
*
* @param host The host to listen to, or null to listen on all hosts
+ * @param port The port to listen to, or 0 to auto select
*/
-class TestHttpServer(host: String? = null) : NanoHTTPD(host, 0 /* auto-select the port */) {
+class TestHttpServer
+ @JvmOverloads constructor(host: String? = null, port: Int = 0) : NanoHTTPD(host, port) {
// Map of URL path -> HTTP response code
private val responses = HashMap<Request, Response>()
diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java
index 3a3459b..3124b1b 100644
--- a/tests/common/java/android/net/NetworkCapabilitiesTest.java
+++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java
@@ -54,6 +54,7 @@
import static android.net.NetworkCapabilities.SIGNAL_STRENGTH_UNSPECIFIED;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_SATELLITE;
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
import static android.net.NetworkCapabilities.TRANSPORT_USB;
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
@@ -761,6 +762,47 @@
}
@Test
+ public void testSetNetworkSpecifierWithCellularAndSatelliteMultiTransportNc() {
+ final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(1);
+ NetworkCapabilities nc = new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addTransportType(TRANSPORT_SATELLITE)
+ .setNetworkSpecifier(specifier)
+ .build();
+ // Adding a specifier did not crash with 2 transports if it is cellular + satellite
+ assertEquals(specifier, nc.getNetworkSpecifier());
+ }
+
+ @Test
+ public void testSetNetworkSpecifierWithWifiAndSatelliteMultiTransportNc() {
+ final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(1);
+ NetworkCapabilities.Builder nc1 = new NetworkCapabilities.Builder();
+ nc1.addTransportType(TRANSPORT_SATELLITE).addTransportType(TRANSPORT_WIFI);
+ // Adding multiple transports specifier to crash, apart from cellular + satellite
+ // combination
+ assertThrows("Cannot set NetworkSpecifier on a NetworkCapability with multiple transports!",
+ IllegalStateException.class,
+ () -> nc1.build().setNetworkSpecifier(specifier));
+ assertThrows("Cannot set NetworkSpecifier on a NetworkCapability with multiple transports!",
+ IllegalStateException.class,
+ () -> nc1.setNetworkSpecifier(specifier));
+ }
+
+ @Test
+ public void testSetNetworkSpecifierOnTestWithCellularAndSatelliteMultiTransportNc() {
+ final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(1);
+ NetworkCapabilities nc = new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_TEST)
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addTransportType(TRANSPORT_SATELLITE)
+ .setNetworkSpecifier(specifier)
+ .build();
+ // Adding a specifier did not crash with 3 transports , TEST + CELLULAR + SATELLITE and if
+ // one is test
+ assertEquals(specifier, nc.getNetworkSpecifier());
+ }
+
+ @Test
public void testSetNetworkSpecifierOnTestMultiTransportNc() {
final NetworkSpecifier specifier = CompatUtil.makeEthernetNetworkSpecifier("eth0");
NetworkCapabilities nc = new NetworkCapabilities.Builder()
diff --git a/tests/cts/net/src/android/net/cts/IpSecTransformStateTest.java b/tests/cts/net/src/android/net/cts/IpSecTransformStateTest.java
new file mode 100644
index 0000000..7b42306
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/IpSecTransformStateTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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 static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.fail;
+
+import android.net.IpSecTransformState;
+import android.os.Build;
+import android.os.SystemClock;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+@RunWith(DevSdkIgnoreRunner.class)
+public class IpSecTransformStateTest {
+ @Rule public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
+
+ private static final long TIMESTAMP_MILLIS = 1000L;
+ private static final long HIGHEST_SEQ_NUMBER_TX = 10000L;
+ private static final long HIGHEST_SEQ_NUMBER_RX = 20000L;
+ private static final long PACKET_COUNT = 9000L;
+ private static final long BYTE_COUNT = 900000L;
+
+ private static final int REPLAY_BITMAP_LEN_BYTE = 512;
+ private static final byte[] REPLAY_BITMAP_NO_PACKETS = new byte[REPLAY_BITMAP_LEN_BYTE];
+ private static final byte[] REPLAY_BITMAP_ALL_RECEIVED = new byte[REPLAY_BITMAP_LEN_BYTE];
+
+ static {
+ for (int i = 0; i < REPLAY_BITMAP_ALL_RECEIVED.length; i++) {
+ REPLAY_BITMAP_ALL_RECEIVED[i] = (byte) 0xff;
+ }
+ }
+
+ @Test
+ public void testBuildAndGet() {
+ final IpSecTransformState state =
+ new IpSecTransformState.Builder()
+ .setTimestampMillis(TIMESTAMP_MILLIS)
+ .setTxHighestSequenceNumber(HIGHEST_SEQ_NUMBER_TX)
+ .setRxHighestSequenceNumber(HIGHEST_SEQ_NUMBER_RX)
+ .setPacketCount(PACKET_COUNT)
+ .setByteCount(BYTE_COUNT)
+ .setReplayBitmap(REPLAY_BITMAP_ALL_RECEIVED)
+ .build();
+
+ assertEquals(TIMESTAMP_MILLIS, state.getTimestampMillis());
+ assertEquals(HIGHEST_SEQ_NUMBER_TX, state.getTxHighestSequenceNumber());
+ assertEquals(HIGHEST_SEQ_NUMBER_RX, state.getRxHighestSequenceNumber());
+ assertEquals(PACKET_COUNT, state.getPacketCount());
+ assertEquals(BYTE_COUNT, state.getByteCount());
+ assertArrayEquals(REPLAY_BITMAP_ALL_RECEIVED, state.getReplayBitmap());
+ }
+
+ @Test
+ public void testSelfGeneratedTimestampMillis() {
+ final long elapsedRealtimeBefore = SystemClock.elapsedRealtime();
+
+ final IpSecTransformState state =
+ new IpSecTransformState.Builder().setReplayBitmap(REPLAY_BITMAP_NO_PACKETS).build();
+
+ final long elapsedRealtimeAfter = SystemClock.elapsedRealtime();
+
+ // Verify elapsedRealtimeBefore <= state.getTimestampMillis() <= elapsedRealtimeAfter
+ assertFalse(elapsedRealtimeBefore > state.getTimestampMillis());
+ assertFalse(elapsedRealtimeAfter < state.getTimestampMillis());
+ }
+
+ @Test
+ public void testBuildWithoutReplayBitmap() throws Exception {
+ try {
+ new IpSecTransformState.Builder().build();
+ fail("Expected expcetion if replay bitmap is not set");
+ } catch (NullPointerException expected) {
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index 6db372f..ce2c2c1 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -1716,6 +1716,177 @@
}
}
+ @Test
+ fun testReplyWhenKnownAnswerSuppressionFlagSet() {
+ // The flag may be removed in the future but known-answer suppression should be enabled by
+ // default in that case. The rule will reset flags automatically on teardown.
+ deviceConfigRule.setConfig(NAMESPACE_TETHERING, "test_nsd_known_answer_suppression", "1")
+ deviceConfigRule.setConfig(NAMESPACE_TETHERING, "test_nsd_unicast_reply_enabled", "1")
+
+ val si = makeTestServiceInfo(testNetwork1.network)
+
+ // Register service on testNetwork1
+ val registrationRecord = NsdRegistrationRecord()
+ var nsResponder: NSResponder? = null
+ tryTest {
+ registerService(registrationRecord, si)
+ val packetReader = TapPacketReader(Handler(handlerThread.looper),
+ testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ packetReader.startAsyncForTest()
+
+ handlerThread.waitForIdle(TIMEOUT_MS)
+ /*
+ Send a query with a known answer. Expect to receive a response containing TXT record
+ only.
+ Generated with:
+ scapy.raw(scapy.DNS(rd=0, qr=0, aa=0, qd =
+ scapy.DNSQR(qname='_nmt123456789._tcp.local', qtype='PTR',
+ qclass=0x8001) /
+ scapy.DNSQR(qname='NsdTest123456789._nmt123456789._tcp.local', qtype='TXT',
+ qclass=0x8001),
+ an = scapy.DNSRR(rrname='_nmt123456789._tcp.local', type='PTR', ttl=4500,
+ rdata='NsdTest123456789._nmt123456789._tcp.local')
+ )).hex()
+ */
+ val query = HexDump.hexStringToByteArray("0000000000020001000000000d5f6e6d74313233343" +
+ "536373839045f746370056c6f63616c00000c8001104e7364546573743132333435363738390" +
+ "d5f6e6d74313233343536373839045f746370056c6f63616c00001080010d5f6e6d743132333" +
+ "43536373839045f746370056c6f63616c00000c000100001194002b104e73645465737431323" +
+ "33435363738390d5f6e6d74313233343536373839045f746370056c6f63616c00")
+ replaceServiceNameAndTypeWithTestSuffix(query)
+
+ val testSrcAddr = makeLinkLocalAddressOfOtherDeviceOnPrefix(testNetwork1.network)
+ nsResponder = NSResponder(packetReader, mapOf(
+ testSrcAddr to MacAddress.fromString("01:02:03:04:05:06")
+ )).apply { start() }
+
+ packetReader.sendResponse(buildMdnsPacket(query, testSrcAddr))
+ // The reply is sent unicast to the source address. There may be announcements sent
+ // multicast around this time, so filter by destination address.
+ val reply = packetReader.pollForMdnsPacket { pkt ->
+ pkt.isReplyFor("$serviceName.$serviceType.local", DnsResolver.TYPE_TXT) &&
+ !pkt.isReplyFor("$serviceType.local", DnsResolver.TYPE_PTR) &&
+ pkt.dstAddr == testSrcAddr
+ }
+ assertNotNull(reply)
+
+ /*
+ Send a query with a known answer (TTL is less than half). Expect to receive a response
+ containing both PTR and TXT records.
+ Generated with:
+ scapy.raw(scapy.DNS(rd=0, qr=0, aa=0, qd =
+ scapy.DNSQR(qname='_nmt123456789._tcp.local', qtype='PTR',
+ qclass=0x8001) /
+ scapy.DNSQR(qname='NsdTest123456789._nmt123456789._tcp.local', qtype='TXT',
+ qclass=0x8001),
+ an = scapy.DNSRR(rrname='_nmt123456789._tcp.local', type='PTR', ttl=2150,
+ rdata='NsdTest123456789._nmt123456789._tcp.local')
+ )).hex()
+ */
+ val query2 = HexDump.hexStringToByteArray("0000000000020001000000000d5f6e6d7431323334" +
+ "3536373839045f746370056c6f63616c00000c8001104e736454657374313233343536373839" +
+ "0d5f6e6d74313233343536373839045f746370056c6f63616c00001080010d5f6e6d74313233" +
+ "343536373839045f746370056c6f63616c00000c000100000866002b104e7364546573743132" +
+ "333435363738390d5f6e6d74313233343536373839045f746370056c6f63616c00")
+ replaceServiceNameAndTypeWithTestSuffix(query2)
+
+ packetReader.sendResponse(buildMdnsPacket(query2, testSrcAddr))
+ // The reply is sent unicast to the source address. There may be announcements sent
+ // multicast around this time, so filter by destination address.
+ val reply2 = packetReader.pollForMdnsPacket { pkt ->
+ pkt.isReplyFor("$serviceName.$serviceType.local", DnsResolver.TYPE_TXT) &&
+ pkt.isReplyFor("$serviceType.local", DnsResolver.TYPE_PTR) &&
+ pkt.dstAddr == testSrcAddr
+ }
+ assertNotNull(reply2)
+ } cleanup {
+ nsResponder?.stop()
+ nsdManager.unregisterService(registrationRecord)
+ registrationRecord.expectCallback<ServiceUnregistered>()
+ }
+ }
+
+ @Test
+ fun testReplyWithMultipacketWhenKnownAnswerSuppressionFlagSet() {
+ // The flag may be removed in the future but known-answer suppression should be enabled by
+ // default in that case. The rule will reset flags automatically on teardown.
+ deviceConfigRule.setConfig(NAMESPACE_TETHERING, "test_nsd_known_answer_suppression", "1")
+ deviceConfigRule.setConfig(NAMESPACE_TETHERING, "test_nsd_unicast_reply_enabled", "1")
+
+ val si = makeTestServiceInfo(testNetwork1.network)
+
+ // Register service on testNetwork1
+ val registrationRecord = NsdRegistrationRecord()
+ var nsResponder: NSResponder? = null
+ tryTest {
+ registerService(registrationRecord, si)
+ val packetReader = TapPacketReader(Handler(handlerThread.looper),
+ testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ packetReader.startAsyncForTest()
+
+ handlerThread.waitForIdle(TIMEOUT_MS)
+ /*
+ Send a query with truncated bit set.
+ Generated with:
+ scapy.raw(scapy.DNS(rd=0, qr=0, aa=0, tc=1, qd=
+ scapy.DNSQR(qname='_nmt123456789._tcp.local', qtype='PTR',
+ qclass=0x8001) /
+ scapy.DNSQR(qname='NsdTest123456789._nmt123456789._tcp.local', qtype='TXT',
+ qclass=0x8001)
+ )).hex()
+ */
+ val query = HexDump.hexStringToByteArray("0000020000020000000000000d5f6e6d74313233343" +
+ "536373839045f746370056c6f63616c00000c8001104e7364546573743132333435363738390" +
+ "d5f6e6d74313233343536373839045f746370056c6f63616c0000108001")
+ replaceServiceNameAndTypeWithTestSuffix(query)
+ /*
+ Send a known answer packet (other service) with truncated bit set.
+ Generated with:
+ scapy.raw(scapy.DNS(rd=0, qr=0, aa=0, tc=1, qd=None,
+ an = scapy.DNSRR(rrname='_test._tcp.local', type='PTR', ttl=4500,
+ rdata='NsdTest._test._tcp.local')
+ )).hex()
+ */
+ val knownAnswer1 = HexDump.hexStringToByteArray("000002000000000100000000055f74657374" +
+ "045f746370056c6f63616c00000c000100001194001a074e736454657374055f74657374045f" +
+ "746370056c6f63616c00")
+ replaceServiceNameAndTypeWithTestSuffix(knownAnswer1)
+ /*
+ Send a known answer packet.
+ Generated with:
+ scapy.raw(scapy.DNS(rd=0, qr=0, aa=0, qd=None,
+ an = scapy.DNSRR(rrname='_nmt123456789._tcp.local', type='PTR', ttl=4500,
+ rdata='NsdTest123456789._nmt123456789._tcp.local')
+ )).hex()
+ */
+ val knownAnswer2 = HexDump.hexStringToByteArray("0000000000000001000000000d5f6e6d7431" +
+ "3233343536373839045f746370056c6f63616c00000c000100001194002b104e736454657374" +
+ "3132333435363738390d5f6e6d74313233343536373839045f746370056c6f63616c00")
+ replaceServiceNameAndTypeWithTestSuffix(knownAnswer2)
+
+ val testSrcAddr = makeLinkLocalAddressOfOtherDeviceOnPrefix(testNetwork1.network)
+ nsResponder = NSResponder(packetReader, mapOf(
+ testSrcAddr to MacAddress.fromString("01:02:03:04:05:06")
+ )).apply { start() }
+
+ packetReader.sendResponse(buildMdnsPacket(query, testSrcAddr))
+ packetReader.sendResponse(buildMdnsPacket(knownAnswer1, testSrcAddr))
+ packetReader.sendResponse(buildMdnsPacket(knownAnswer2, testSrcAddr))
+ // The reply is sent unicast to the source address. There may be announcements sent
+ // multicast around this time, so filter by destination address.
+ val reply = packetReader.pollForMdnsPacket { pkt ->
+ pkt.isReplyFor("$serviceName.$serviceType.local", DnsResolver.TYPE_TXT) &&
+ !pkt.isReplyFor("$serviceType.local", DnsResolver.TYPE_PTR) &&
+ pkt.dstAddr == testSrcAddr
+ }
+ assertNotNull(reply)
+ } cleanup {
+ nsResponder?.stop()
+ nsdManager.unregisterService(registrationRecord)
+ registrationRecord.expectCallback<ServiceUnregistered>()
+ }
+ }
+
private fun makeLinkLocalAddressOfOtherDeviceOnPrefix(network: Network): Inet6Address {
val lp = cm.getLinkProperties(network) ?: fail("No LinkProperties for net $network")
// Expect to have a /64 link-local address
diff --git a/tests/unit/java/com/android/server/connectivity/SatelliteAccessControllerTest.kt b/tests/unit/java/com/android/server/connectivity/SatelliteAccessControllerTest.kt
index 64a515a..193078b 100644
--- a/tests/unit/java/com/android/server/connectivity/SatelliteAccessControllerTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/SatelliteAccessControllerTest.kt
@@ -21,9 +21,12 @@
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
+import android.content.pm.UserInfo
import android.os.Build
import android.os.Handler
import android.os.UserHandle
+import android.util.ArraySet
+import com.android.server.makeMockUserManager
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
import com.android.testutils.DevSdkIgnoreRunner
import org.junit.Before
@@ -36,18 +39,31 @@
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
-import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import java.util.concurrent.Executor
import java.util.function.Consumer
-import kotlin.test.assertEquals
-import kotlin.test.assertFalse
-import kotlin.test.assertTrue
-private const val DEFAULT_MESSAGING_APP1 = "default_messaging_app_1"
-private const val DEFAULT_MESSAGING_APP2 = "default_messaging_app_2"
-private const val DEFAULT_MESSAGING_APP1_UID = 1234
-private const val DEFAULT_MESSAGING_APP2_UID = 5678
+private const val USER = 0
+val USER_INFO = UserInfo(USER, "" /* name */, UserInfo.FLAG_PRIMARY)
+val USER_HANDLE = UserHandle(USER)
+private const val PRIMARY_USER = 0
+private const val SECONDARY_USER = 10
+private val PRIMARY_USER_HANDLE = UserHandle.of(PRIMARY_USER)
+private val SECONDARY_USER_HANDLE = UserHandle.of(SECONDARY_USER)
+// sms app names
+private const val SMS_APP1 = "sms_app_1"
+private const val SMS_APP2 = "sms_app_2"
+// sms app ids
+private const val SMS_APP_ID1 = 100
+private const val SMS_APP_ID2 = 101
+// UID for app1 and app2 on primary user
+// These app could become default sms app for user1
+private val PRIMARY_USER_SMS_APP_UID1 = UserHandle.getUid(PRIMARY_USER, SMS_APP_ID1)
+private val PRIMARY_USER_SMS_APP_UID2 = UserHandle.getUid(PRIMARY_USER, SMS_APP_ID2)
+// UID for app1 and app2 on secondary user
+// These app could become default sms app for user2
+private val SECONDARY_USER_SMS_APP_UID1 = UserHandle.getUid(SECONDARY_USER, SMS_APP_ID1)
+private val SECONDARY_USER_SMS_APP_UID2 = UserHandle.getUid(SECONDARY_USER, SMS_APP_ID2)
@RunWith(DevSdkIgnoreRunner::class)
@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
@@ -58,33 +74,36 @@
private val mRoleManager =
mock(SatelliteAccessController.Dependencies::class.java)
private val mCallback = mock(Consumer::class.java) as Consumer<Set<Int>>
- private val mSatelliteAccessController by lazy {
- SatelliteAccessController(context, mRoleManager, mCallback, mHandler)}
- private var mRoleHolderChangedListener: OnRoleHoldersChangedListener? = null
+ private val mSatelliteAccessController =
+ SatelliteAccessController(context, mRoleManager, mCallback, mHandler)
+ private lateinit var mRoleHolderChangedListener: OnRoleHoldersChangedListener
@Before
@Throws(PackageManager.NameNotFoundException::class)
fun setup() {
+ makeMockUserManager(USER_INFO, USER_HANDLE)
+ doReturn(context).`when`(context).createContextAsUser(any(), anyInt())
doReturn(mPackageManager).`when`(context).packageManager
- doReturn(PackageManager.PERMISSION_GRANTED)
- .`when`(mPackageManager)
- .checkPermission(Manifest.permission.SATELLITE_COMMUNICATION, DEFAULT_MESSAGING_APP1)
- doReturn(PackageManager.PERMISSION_GRANTED)
- .`when`(mPackageManager)
- .checkPermission(Manifest.permission.SATELLITE_COMMUNICATION, DEFAULT_MESSAGING_APP2)
- // Initialise default message application package1
+ doReturn(PackageManager.PERMISSION_GRANTED)
+ .`when`(mPackageManager)
+ .checkPermission(Manifest.permission.SATELLITE_COMMUNICATION, SMS_APP1)
+ doReturn(PackageManager.PERMISSION_GRANTED)
+ .`when`(mPackageManager)
+ .checkPermission(Manifest.permission.SATELLITE_COMMUNICATION, SMS_APP2)
+
+ // Initialise default message application primary user package1
val applicationInfo1 = ApplicationInfo()
- applicationInfo1.uid = DEFAULT_MESSAGING_APP1_UID
+ applicationInfo1.uid = PRIMARY_USER_SMS_APP_UID1
doReturn(applicationInfo1)
.`when`(mPackageManager)
- .getApplicationInfo(eq(DEFAULT_MESSAGING_APP1), anyInt())
+ .getApplicationInfo(eq(SMS_APP1), anyInt())
- // Initialise default message application package2
+ // Initialise default message application primary user package2
val applicationInfo2 = ApplicationInfo()
- applicationInfo2.uid = DEFAULT_MESSAGING_APP2_UID
+ applicationInfo2.uid = PRIMARY_USER_SMS_APP_UID2
doReturn(applicationInfo2)
.`when`(mPackageManager)
- .getApplicationInfo(eq(DEFAULT_MESSAGING_APP2), anyInt())
+ .getApplicationInfo(eq(SMS_APP2), anyInt())
// Get registered listener using captor
val listenerCaptor = ArgumentCaptor.forClass(
@@ -97,80 +116,107 @@
}
@Test
- fun test_onRoleHoldersChanged_SatellitePreferredUid_Changed() {
- doReturn(listOf<String>()).`when`(mRoleManager).getRoleHolders(RoleManager.ROLE_SMS)
- val satelliteNetworkPreferredSet =
- ArgumentCaptor.forClass(Set::class.java) as ArgumentCaptor<Set<Int>>
- mRoleHolderChangedListener?.onRoleHoldersChanged(RoleManager.ROLE_SMS, UserHandle.ALL)
- verify(mCallback, never()).accept(satelliteNetworkPreferredSet.capture())
+ fun test_onRoleHoldersChanged_SatelliteFallbackUid_Changed_SingleUser() {
+ doReturn(listOf<String>()).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+ PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback, never()).accept(any())
- // check DEFAULT_MESSAGING_APP1 is available as satellite network preferred uid
- doReturn(listOf(DEFAULT_MESSAGING_APP1))
- .`when`(mRoleManager).getRoleHolders(RoleManager.ROLE_SMS)
- mRoleHolderChangedListener?.onRoleHoldersChanged(RoleManager.ROLE_SMS, UserHandle.ALL)
- verify(mCallback).accept(satelliteNetworkPreferredSet.capture())
- var satelliteNetworkPreferredUids = satelliteNetworkPreferredSet.value
- assertEquals(1, satelliteNetworkPreferredUids.size)
- assertTrue(satelliteNetworkPreferredUids.contains(DEFAULT_MESSAGING_APP1_UID))
- assertFalse(satelliteNetworkPreferredUids.contains(DEFAULT_MESSAGING_APP2_UID))
+ // check DEFAULT_MESSAGING_APP1 is available as satellite network fallback uid
+ doReturn(listOf(SMS_APP1))
+ .`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID1))
- // check DEFAULT_MESSAGING_APP1 and DEFAULT_MESSAGING_APP2 is available
- // as satellite network preferred uid
- val dmas: MutableList<String> = ArrayList()
- dmas.add(DEFAULT_MESSAGING_APP1)
- dmas.add(DEFAULT_MESSAGING_APP2)
- doReturn(dmas).`when`(mRoleManager).getRoleHolders(RoleManager.ROLE_SMS)
- mRoleHolderChangedListener?.onRoleHoldersChanged(RoleManager.ROLE_SMS, UserHandle.ALL)
- verify(mCallback, times(2))
- .accept(satelliteNetworkPreferredSet.capture())
- satelliteNetworkPreferredUids = satelliteNetworkPreferredSet.value
- assertEquals(2, satelliteNetworkPreferredUids.size)
- assertTrue(satelliteNetworkPreferredUids.contains(DEFAULT_MESSAGING_APP1_UID))
- assertTrue(satelliteNetworkPreferredUids.contains(DEFAULT_MESSAGING_APP2_UID))
+ // check SMS_APP2 is available as satellite network Fallback uid
+ doReturn(listOf(SMS_APP2)).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+ PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID2))
- // check no uid is available as satellite network preferred uid
- doReturn(listOf<String>()).`when`(mRoleManager).getRoleHolders(RoleManager.ROLE_SMS)
- mRoleHolderChangedListener?.onRoleHoldersChanged(RoleManager.ROLE_SMS, UserHandle.ALL)
- verify(mCallback, times(3))
- .accept(satelliteNetworkPreferredSet.capture())
- satelliteNetworkPreferredUids = satelliteNetworkPreferredSet.value
- assertEquals(0, satelliteNetworkPreferredUids.size)
- assertFalse(satelliteNetworkPreferredUids.contains(DEFAULT_MESSAGING_APP1_UID))
- assertFalse(satelliteNetworkPreferredUids.contains(DEFAULT_MESSAGING_APP2_UID))
-
- // No Change received at OnRoleSmsChanged, check callback not triggered
- doReturn(listOf<String>()).`when`(mRoleManager).getRoleHolders(RoleManager.ROLE_SMS)
- mRoleHolderChangedListener?.onRoleHoldersChanged(RoleManager.ROLE_SMS, UserHandle.ALL)
- verify(mCallback, times(3))
- .accept(satelliteNetworkPreferredSet.capture())
+ // check no uid is available as satellite network fallback uid
+ doReturn(listOf<String>()).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+ PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback).accept(ArraySet())
}
@Test
fun test_onRoleHoldersChanged_NoSatelliteCommunicationPermission() {
- doReturn(listOf<Any>()).`when`(mRoleManager).getRoleHolders(RoleManager.ROLE_SMS)
- val satelliteNetworkPreferredSet =
- ArgumentCaptor.forClass(Set::class.java) as ArgumentCaptor<Set<Int>>
- mRoleHolderChangedListener?.onRoleHoldersChanged(RoleManager.ROLE_SMS, UserHandle.ALL)
- verify(mCallback, never()).accept(satelliteNetworkPreferredSet.capture())
+ doReturn(listOf<Any>()).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+ PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback, never()).accept(any())
- // check DEFAULT_MESSAGING_APP1 is not available as satellite network preferred uid
+ // check DEFAULT_MESSAGING_APP1 is not available as satellite network fallback uid
// since satellite communication permission not available.
doReturn(PackageManager.PERMISSION_DENIED)
.`when`(mPackageManager)
- .checkPermission(Manifest.permission.SATELLITE_COMMUNICATION, DEFAULT_MESSAGING_APP1)
- doReturn(listOf(DEFAULT_MESSAGING_APP1))
- .`when`(mRoleManager).getRoleHolders(RoleManager.ROLE_SMS)
- mRoleHolderChangedListener?.onRoleHoldersChanged(RoleManager.ROLE_SMS, UserHandle.ALL)
- verify(mCallback, never()).accept(satelliteNetworkPreferredSet.capture())
+ .checkPermission(Manifest.permission.SATELLITE_COMMUNICATION, SMS_APP1)
+ doReturn(listOf(SMS_APP1))
+ .`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback, never()).accept(any())
}
@Test
fun test_onRoleHoldersChanged_RoleSms_NotAvailable() {
- doReturn(listOf(DEFAULT_MESSAGING_APP1))
- .`when`(mRoleManager).getRoleHolders(RoleManager.ROLE_SMS)
- val satelliteNetworkPreferredSet =
- ArgumentCaptor.forClass(Set::class.java) as ArgumentCaptor<Set<Int>>
- mRoleHolderChangedListener?.onRoleHoldersChanged(RoleManager.ROLE_BROWSER, UserHandle.ALL)
- verify(mCallback, never()).accept(satelliteNetworkPreferredSet.capture())
+ doReturn(listOf(SMS_APP1))
+ .`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_BROWSER,
+ PRIMARY_USER_HANDLE)
+ verify(mCallback, never()).accept(any())
+ }
+
+ @Test
+ fun test_onRoleHoldersChanged_SatelliteNetworkFallbackUid_Changed_multiUser() {
+ doReturn(listOf<String>()).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+ PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback, never()).accept(any())
+
+ // check SMS_APP1 is available as satellite network fallback uid at primary user
+ doReturn(listOf(SMS_APP1))
+ .`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID1))
+
+ // check SMS_APP2 is available as satellite network fallback uid at primary user
+ doReturn(listOf(SMS_APP2)).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+ PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID2))
+
+ // check SMS_APP1 is available as satellite network fallback uid at secondary user
+ val applicationInfo1 = ApplicationInfo()
+ applicationInfo1.uid = SECONDARY_USER_SMS_APP_UID1
+ doReturn(applicationInfo1).`when`(mPackageManager)
+ .getApplicationInfo(eq(SMS_APP1), anyInt())
+ doReturn(listOf(SMS_APP1)).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+ SECONDARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, SECONDARY_USER_HANDLE)
+ verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID2, SECONDARY_USER_SMS_APP_UID1))
+
+ // check no uid is available as satellite network fallback uid at primary user
+ doReturn(listOf<String>()).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+ PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS,
+ PRIMARY_USER_HANDLE)
+ verify(mCallback).accept(setOf(SECONDARY_USER_SMS_APP_UID1))
+
+ // check SMS_APP2 is available as satellite network fallback uid at secondary user
+ applicationInfo1.uid = SECONDARY_USER_SMS_APP_UID2
+ doReturn(applicationInfo1).`when`(mPackageManager)
+ .getApplicationInfo(eq(SMS_APP2), anyInt())
+ doReturn(listOf(SMS_APP2))
+ .`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS, SECONDARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, SECONDARY_USER_HANDLE)
+ verify(mCallback).accept(setOf(SECONDARY_USER_SMS_APP_UID2))
+
+ // check no uid is available as satellite network fallback uid at secondary user
+ doReturn(listOf<String>()).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+ SECONDARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, SECONDARY_USER_HANDLE)
+ verify(mCallback).accept(ArraySet())
}
}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSCaptivePortalAppTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSCaptivePortalAppTest.kt
new file mode 100644
index 0000000..be2b29c
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSCaptivePortalAppTest.kt
@@ -0,0 +1,127 @@
+/*
+ * 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.Manifest.permission.NETWORK_STACK
+import android.content.Intent
+import android.content.pm.PackageManager.PERMISSION_DENIED
+import android.content.pm.PackageManager.PERMISSION_GRANTED
+import android.net.ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN
+import android.net.ConnectivityManager.EXTRA_CAPTIVE_PORTAL
+import android.net.IpPrefix
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkStack
+import android.net.CaptivePortal
+import android.net.NetworkRequest
+import android.net.NetworkScore
+import android.net.NetworkScore.KEEP_CONNECTED_FOR_TEST
+import android.net.RouteInfo
+import android.os.Build
+import android.os.Bundle
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.assertThrows
+import com.android.testutils.TestableNetworkCallback
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+import kotlin.test.assertEquals
+
+// This allows keeping all the networks connected without having to file individual requests
+// for them.
+private fun keepScore() = FromS(
+ NetworkScore.Builder().setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST).build()
+)
+
+private fun nc(transport: Int, vararg caps: Int) = NetworkCapabilities.Builder().apply {
+ addTransportType(transport)
+ caps.forEach {
+ addCapability(it)
+ }
+ // Useful capabilities for everybody
+ addCapability(NET_CAPABILITY_NOT_RESTRICTED)
+ addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ addCapability(NET_CAPABILITY_NOT_ROAMING)
+ addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+}.build()
+
+private fun lp(iface: String) = LinkProperties().apply {
+ interfaceName = iface
+ addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
+ addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
+}
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.R)
+class CSCaptivePortalAppTest : CSTest() {
+ private val WIFI_IFACE = "wifi0"
+ private val TEST_REDIRECT_URL = "http://example.com/firstPath"
+ private val TIMEOUT_MS = 2_000L
+
+ @Test
+ fun testCaptivePortalApp_Reevaluate_Nopermission() {
+ val captivePortalCallback = TestableNetworkCallback()
+ val captivePortalRequest = NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_CAPTIVE_PORTAL).build()
+ cm.registerNetworkCallback(captivePortalRequest, captivePortalCallback)
+ val wifiAgent = createWifiAgent()
+ wifiAgent.connectWithCaptivePortal(TEST_REDIRECT_URL)
+ captivePortalCallback.expectAvailableCallbacksUnvalidated(wifiAgent)
+ val signInIntent = startCaptivePortalApp(wifiAgent)
+ // Remove the granted permissions
+ context.setPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+ PERMISSION_DENIED)
+ context.setPermission(NETWORK_STACK, PERMISSION_DENIED)
+ val captivePortal: CaptivePortal? = signInIntent.getParcelableExtra(EXTRA_CAPTIVE_PORTAL)
+ assertThrows(SecurityException::class.java, { captivePortal?.reevaluateNetwork() })
+ }
+
+ private fun createWifiAgent(): CSAgentWrapper {
+ return Agent(score = keepScore(), lp = lp(WIFI_IFACE),
+ nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET))
+ }
+
+ private fun startCaptivePortalApp(networkAgent: CSAgentWrapper): Intent {
+ val network = networkAgent.network
+ cm.startCaptivePortalApp(network)
+ waitForIdle()
+ verify(networkAgent.networkMonitor).launchCaptivePortalApp()
+
+ val testBundle = Bundle()
+ val testKey = "testkey"
+ val testValue = "testvalue"
+ testBundle.putString(testKey, testValue)
+ context.setPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, PERMISSION_GRANTED)
+ cm.startCaptivePortalApp(network, testBundle)
+ val signInIntent: Intent = context.expectStartActivityIntent(TIMEOUT_MS)
+ assertEquals(ACTION_CAPTIVE_PORTAL_SIGN_IN, signInIntent.getAction())
+ assertEquals(testValue, signInIntent.getStringExtra(testKey))
+ return signInIntent
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
index d41c742..d7343b1 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
@@ -19,6 +19,8 @@
import android.content.Context
import android.net.ConnectivityManager
import android.net.INetworkMonitor
+import android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_DNS
+import android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_HTTP
import android.net.INetworkMonitorCallbacks
import android.net.LinkProperties
import android.net.LocalNetworkConfig
@@ -75,10 +77,15 @@
) : TestableNetworkCallback.HasNetwork {
private val TAG = "CSAgent${nextAgentId()}"
private val VALIDATION_RESULT_INVALID = 0
+ private val NO_PROBE_RESULT = 0
private val VALIDATION_TIMESTAMP = 1234L
private val agent: NetworkAgent
private val nmCallbacks: INetworkMonitorCallbacks
val networkMonitor = mock<INetworkMonitor>()
+ private var nmValidationRedirectUrl: String? = null
+ private var nmValidationResult = NO_PROBE_RESULT
+ private var nmProbesCompleted = NO_PROBE_RESULT
+ private var nmProbesSucceeded = NO_PROBE_RESULT
override val network: Network get() = agent.network!!
@@ -120,10 +127,10 @@
}
nmCallbacks.notifyProbeStatusChanged(0 /* completed */, 0 /* succeeded */)
val p = NetworkTestResultParcelable()
- p.result = VALIDATION_RESULT_INVALID
- p.probesAttempted = 0
- p.probesSucceeded = 0
- p.redirectUrl = null
+ p.result = nmValidationResult
+ p.probesAttempted = nmProbesCompleted
+ p.probesSucceeded = nmProbesSucceeded
+ p.redirectUrl = nmValidationRedirectUrl
p.timestampMillis = VALIDATION_TIMESTAMP
nmCallbacks.notifyNetworkTestedWithExtras(p)
}
@@ -171,4 +178,26 @@
fun sendLocalNetworkConfig(lnc: LocalNetworkConfig) = agent.sendLocalNetworkConfig(lnc)
fun sendNetworkCapabilities(nc: NetworkCapabilities) = agent.sendNetworkCapabilities(nc)
+
+ fun connectWithCaptivePortal(redirectUrl: String) {
+ setCaptivePortal(redirectUrl)
+ connect()
+ }
+
+ fun setProbesStatus(probesCompleted: Int, probesSucceeded: Int) {
+ nmProbesCompleted = probesCompleted
+ nmProbesSucceeded = probesSucceeded
+ }
+
+ fun setCaptivePortal(redirectUrl: String) {
+ nmValidationResult = VALIDATION_RESULT_INVALID
+ nmValidationRedirectUrl = redirectUrl
+ // Suppose the portal is found when NetworkMonitor probes NETWORK_VALIDATION_PROBE_HTTP
+ // in the beginning. Because NETWORK_VALIDATION_PROBE_HTTP is the decisive probe for captive
+ // portal, considering the NETWORK_VALIDATION_PROBE_HTTPS hasn't probed yet and set only
+ // DNS and HTTP probes completed.
+ setProbesStatus(
+ NETWORK_VALIDATION_PROBE_DNS or NETWORK_VALIDATION_PROBE_HTTP /* probesCompleted */,
+ VALIDATION_RESULT_INVALID /* probesSucceeded */)
+ }
}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
index e401434..595ca47 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -47,8 +47,10 @@
import android.os.Bundle
import android.os.Handler
import android.os.HandlerThread
+import android.os.Process
import android.os.UserHandle
import android.os.UserManager
+import android.permission.PermissionManager.PermissionResult
import android.telephony.TelephonyManager
import android.testing.TestableContext
import android.util.ArraySet
@@ -71,8 +73,11 @@
import com.android.testutils.visibleOnHandlerThread
import com.android.testutils.waitForIdle
import java.util.concurrent.Executors
+import java.util.concurrent.LinkedBlockingQueue
+import java.util.concurrent.TimeUnit
import java.util.function.Consumer
import java.util.function.BiConsumer
+import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.fail
import org.junit.After
@@ -82,7 +87,7 @@
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock
-internal const val HANDLER_TIMEOUT_MS = 2_000
+internal const val HANDLER_TIMEOUT_MS = 2_000L
internal const val BROADCAST_TIMEOUT_MS = 3_000L
internal const val TEST_PACKAGE_NAME = "com.android.test.package"
internal const val WIFI_WOL_IFNAME = "test_wlan_wol"
@@ -300,13 +305,65 @@
val pacProxyManager = mock<PacProxyManager>()
val networkPolicyManager = mock<NetworkPolicyManager>()
+ // Map of permission name -> PermissionManager.Permission_{GRANTED|DENIED} constant
+ // For permissions granted across the board, the key is only the permission name.
+ // For permissions only granted to a combination of uid/pid, the key
+ // is "<permission name>,<pid>,<uid>". PID+UID permissions have priority over generic ones.
+ private val mMockedPermissions: HashMap<String, Int> = HashMap()
+ private val mStartedActivities = LinkedBlockingQueue<Intent>()
override fun getPackageManager() = this@CSTest.packageManager
override fun getContentResolver() = this@CSTest.contentResolver
- // TODO : buff up the capabilities of this permission scheme to allow checking for
- // permission rejections
- override fun checkPermission(permission: String, pid: Int, uid: Int) = PERMISSION_GRANTED
- override fun checkCallingOrSelfPermission(permission: String) = PERMISSION_GRANTED
+ // If the permission result does not set in the mMockedPermissions, it will be
+ // considered as PERMISSION_GRANTED as existing design to prevent breaking other tests.
+ override fun checkPermission(permission: String, pid: Int, uid: Int) =
+ checkMockedPermission(permission, pid, uid, PERMISSION_GRANTED)
+
+ override fun enforceCallingOrSelfPermission(permission: String, message: String?) {
+ // If the permission result does not set in the mMockedPermissions, it will be
+ // considered as PERMISSION_GRANTED as existing design to prevent breaking other tests.
+ val granted = checkMockedPermission(permission, Process.myPid(), Process.myUid(),
+ PERMISSION_GRANTED)
+ if (!granted.equals(PERMISSION_GRANTED)) {
+ throw SecurityException("[Test] permission denied: " + permission)
+ }
+ }
+
+ // If the permission result does not set in the mMockedPermissions, it will be
+ // considered as PERMISSION_GRANTED as existing design to prevent breaking other tests.
+ override fun checkCallingOrSelfPermission(permission: String) =
+ checkMockedPermission(permission, Process.myPid(), Process.myUid(), PERMISSION_GRANTED)
+
+ private fun checkMockedPermission(permission: String, pid: Int, uid: Int, default: Int):
+ Int {
+ val processSpecificKey = "$permission,$pid,$uid"
+ return mMockedPermissions[processSpecificKey]
+ ?: mMockedPermissions[permission] ?: default
+ }
+
+ /**
+ * Mock checks for the specified permission, and have them behave as per `granted` or
+ * `denied`.
+ *
+ * This will apply to all calls no matter what the checked UID and PID are.
+ *
+ * @param granted One of {@link PackageManager#PermissionResult}.
+ */
+ fun setPermission(permission: String, @PermissionResult granted: Int) {
+ mMockedPermissions.put(permission, granted)
+ }
+
+ /**
+ * Mock checks for the specified permission, and have them behave as per `granted` or
+ * `denied`.
+ *
+ * This will only apply to the passed UID and PID.
+ *
+ * @param granted One of {@link PackageManager#PermissionResult}.
+ */
+ fun setPermission(permission: String, pid: Int, uid: Int, @PermissionResult granted: Int) {
+ mMockedPermissions.put("$permission,$pid,$uid", granted)
+ }
// Necessary for MultinetworkPolicyTracker, which tries to register a receiver for
// all users. The test can't do that since it doesn't hold INTERACT_ACROSS_USERS.
@@ -364,6 +421,16 @@
) {
orderedBroadcastAsUserHistory.add(intent)
}
+
+ override fun startActivityAsUser(intent: Intent, handle: UserHandle) {
+ mStartedActivities.put(intent)
+ }
+
+ fun expectStartActivityIntent(timeoutMs: Long = HANDLER_TIMEOUT_MS): Intent {
+ val intent = mStartedActivities.poll(timeoutMs, TimeUnit.MILLISECONDS)
+ assertNotNull(intent, "Did not receive sign-in intent after " + timeoutMs + "ms")
+ return intent
+ }
}
// Utility methods for subclasses to use
diff --git a/tests/unit/java/com/android/server/net/TrafficStatsRateLimitCacheTest.kt b/tests/unit/java/com/android/server/net/TrafficStatsRateLimitCacheTest.kt
new file mode 100644
index 0000000..27e6f96
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/TrafficStatsRateLimitCacheTest.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net
+
+import android.net.NetworkStats
+import com.android.testutils.DevSdkIgnoreRunner
+import java.time.Clock
+import kotlin.test.assertEquals
+import kotlin.test.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.`when`
+
+@RunWith(DevSdkIgnoreRunner::class)
+class TrafficStatsRateLimitCacheTest {
+ companion object {
+ private const val expiryDurationMs = 1000L
+ }
+
+ private val clock = mock(Clock::class.java)
+ private val entry = mock(NetworkStats.Entry::class.java)
+ private val cache = TrafficStatsRateLimitCache(clock, expiryDurationMs)
+
+ @Test
+ fun testGet_returnsEntryIfNotExpired() {
+ cache.put("iface", 2, entry)
+ `when`(clock.millis()).thenReturn(500L) // Set clock to before expiry
+ val result = cache.get("iface", 2)
+ assertEquals(entry, result)
+ }
+
+ @Test
+ fun testGet_returnsNullIfExpired() {
+ cache.put("iface", 2, entry)
+ `when`(clock.millis()).thenReturn(2000L) // Set clock to after expiry
+ assertNull(cache.get("iface", 2))
+ }
+
+ @Test
+ fun testGet_returnsNullForNonExistentKey() {
+ val result = cache.get("otherIface", 99)
+ assertNull(result)
+ }
+
+ @Test
+ fun testPutAndGet_retrievesCorrectEntryForDifferentKeys() {
+ val entry1 = mock(NetworkStats.Entry::class.java)
+ val entry2 = mock(NetworkStats.Entry::class.java)
+
+ cache.put("iface1", 2, entry1)
+ cache.put("iface2", 4, entry2)
+
+ assertEquals(entry1, cache.get("iface1", 2))
+ assertEquals(entry2, cache.get("iface2", 4))
+ }
+
+ @Test
+ fun testPut_overridesExistingEntry() {
+ val entry1 = mock(NetworkStats.Entry::class.java)
+ val entry2 = mock(NetworkStats.Entry::class.java)
+
+ cache.put("iface", 2, entry1)
+ cache.put("iface", 2, entry2) // Put with the same key
+
+ assertEquals(entry2, cache.get("iface", 2))
+ }
+
+ @Test
+ fun testClear() {
+ cache.put("iface", 2, entry)
+ cache.clear()
+ assertNull(cache.get("iface", 2))
+ }
+}
diff --git a/thread/flags/Android.bp b/thread/flags/Android.bp
new file mode 100644
index 0000000..15f58a9
--- /dev/null
+++ b/thread/flags/Android.bp
@@ -0,0 +1,23 @@
+//
+// 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.
+//
+
+aconfig_declarations {
+ name: "com.android.net.thread.flags-aconfig",
+ package: "com.android.net.thread.flags",
+ container: "system",
+ srcs: ["thread_base.aconfig"],
+ visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkManager.java b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
index b584487..150b759 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkManager.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
@@ -83,8 +83,8 @@
* This user restriction specifies if Thread network is disallowed on the device. If Thread
* network is disallowed it cannot be turned on via Settings.
*
- * <p>this is a mirror of {@link UserManager#DISALLOW_THREAD_NETWORK} which is not available
- * on Android U devices.
+ * <p>this is a mirror of {@link UserManager#DISALLOW_THREAD_NETWORK} which is not available on
+ * Android U devices.
*
* @hide
*/
diff --git a/thread/service/java/com/android/server/thread/ActiveOperationalDatasetReceiverWrapper.java b/thread/service/java/com/android/server/thread/ActiveOperationalDatasetReceiverWrapper.java
new file mode 100644
index 0000000..e3b4e1a
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/ActiveOperationalDatasetReceiverWrapper.java
@@ -0,0 +1,87 @@
+/*
+ * 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.thread;
+
+import static android.net.thread.ThreadNetworkException.ERROR_UNAVAILABLE;
+
+import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.IActiveOperationalDatasetReceiver;
+import android.os.RemoteException;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A {@link IActiveOperationalDatasetReceiver} wrapper which makes it easier to invoke the
+ * callbacks.
+ */
+final class ActiveOperationalDatasetReceiverWrapper {
+ private final IActiveOperationalDatasetReceiver mReceiver;
+
+ private static final Object sPendingReceiversLock = new Object();
+
+ @GuardedBy("sPendingReceiversLock")
+ private static final Set<ActiveOperationalDatasetReceiverWrapper> sPendingReceivers =
+ new HashSet<>();
+
+ public ActiveOperationalDatasetReceiverWrapper(IActiveOperationalDatasetReceiver receiver) {
+ this.mReceiver = receiver;
+
+ synchronized (sPendingReceiversLock) {
+ sPendingReceivers.add(this);
+ }
+ }
+
+ public static void onOtDaemonDied() {
+ synchronized (sPendingReceiversLock) {
+ for (ActiveOperationalDatasetReceiverWrapper receiver : sPendingReceivers) {
+ try {
+ receiver.mReceiver.onError(ERROR_UNAVAILABLE, "Thread daemon died");
+ } catch (RemoteException e) {
+ // The client is dead, do nothing
+ }
+ }
+ sPendingReceivers.clear();
+ }
+ }
+
+ public void onSuccess(ActiveOperationalDataset dataset) {
+ synchronized (sPendingReceiversLock) {
+ sPendingReceivers.remove(this);
+ }
+
+ try {
+ mReceiver.onSuccess(dataset);
+ } catch (RemoteException e) {
+ // The client is dead, do nothing
+ }
+ }
+
+ public void onError(int errorCode, String errorMessage) {
+ synchronized (sPendingReceiversLock) {
+ sPendingReceivers.remove(this);
+ }
+
+ try {
+ mReceiver.onError(errorCode, errorMessage);
+ } catch (RemoteException e) {
+ // The client is dead, do nothing
+ }
+ }
+}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 44745b3..56dd056 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -46,7 +46,7 @@
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_ABORT;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_BUSY;
-import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_DETACHED;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_FAILED_PRECONDITION;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_INVALID_STATE;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_NO_BUFS;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_PARSE;
@@ -109,6 +109,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.ServiceManagerWrapper;
import com.android.server.thread.openthread.BorderRouterConfigurationParcel;
+import com.android.server.thread.openthread.IChannelMasksReceiver;
import com.android.server.thread.openthread.IOtDaemon;
import com.android.server.thread.openthread.IOtDaemonCallback;
import com.android.server.thread.openthread.IOtStatusReceiver;
@@ -157,9 +158,6 @@
private final NsdPublisher mNsdPublisher;
private final OtDaemonCallbackProxy mOtDaemonCallbackProxy = new OtDaemonCallbackProxy();
- // TODO(b/308310823): read supported channel from Thread dameon
- private final int mSupportedChannelMask = 0x07FFF800; // from channel 11 to 26
-
@Nullable private IOtDaemon mOtDaemon;
@Nullable private NetworkAgent mNetworkAgent;
@Nullable private NetworkAgent mTestNetworkAgent;
@@ -593,26 +591,51 @@
@Override
public void createRandomizedDataset(
String networkName, IActiveOperationalDatasetReceiver receiver) {
- mHandler.post(
- () -> {
- ActiveOperationalDataset dataset =
- createRandomizedDatasetInternal(
- networkName,
- mSupportedChannelMask,
- Instant.now(),
- new Random(),
- new SecureRandom());
- try {
- receiver.onSuccess(dataset);
- } catch (RemoteException e) {
- // The client is dead, do nothing
- }
- });
+ ActiveOperationalDatasetReceiverWrapper receiverWrapper =
+ new ActiveOperationalDatasetReceiverWrapper(receiver);
+ mHandler.post(() -> createRandomizedDatasetInternal(networkName, receiverWrapper));
}
- private static ActiveOperationalDataset createRandomizedDatasetInternal(
+ private void createRandomizedDatasetInternal(
+ String networkName, @NonNull ActiveOperationalDatasetReceiverWrapper receiver) {
+ checkOnHandlerThread();
+
+ try {
+ getOtDaemon().getChannelMasks(newChannelMasksReceiver(networkName, receiver));
+ } catch (RemoteException e) {
+ Log.e(TAG, "otDaemon.getChannelMasks failed", e);
+ receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+ }
+ }
+
+ private IChannelMasksReceiver newChannelMasksReceiver(
+ String networkName, ActiveOperationalDatasetReceiverWrapper receiver) {
+ return new IChannelMasksReceiver.Stub() {
+ @Override
+ public void onSuccess(int supportedChannelMask, int preferredChannelMask) {
+ ActiveOperationalDataset dataset =
+ createRandomizedDataset(
+ networkName,
+ supportedChannelMask,
+ preferredChannelMask,
+ Instant.now(),
+ new Random(),
+ new SecureRandom());
+
+ receiver.onSuccess(dataset);
+ }
+
+ @Override
+ public void onError(int errorCode, String errorMessage) {
+ receiver.onError(otErrorToAndroidError(errorCode), errorMessage);
+ }
+ };
+ }
+
+ private static ActiveOperationalDataset createRandomizedDataset(
String networkName,
int supportedChannelMask,
+ int preferredChannelMask,
Instant now,
Random random,
SecureRandom secureRandom) {
@@ -622,6 +645,7 @@
final SparseArray<byte[]> channelMask = new SparseArray<>(1);
channelMask.put(CHANNEL_PAGE_24_GHZ, channelMaskToByteArray(supportedChannelMask));
+ final int channel = selectChannel(supportedChannelMask, preferredChannelMask, random);
final byte[] securityFlags = new byte[] {(byte) 0xff, (byte) 0xf8};
@@ -632,7 +656,7 @@
.setExtendedPanId(newRandomBytes(random, LENGTH_EXTENDED_PAN_ID))
.setPanId(panId)
.setNetworkName(networkName)
- .setChannel(CHANNEL_PAGE_24_GHZ, selectRandomChannel(supportedChannelMask, random))
+ .setChannel(CHANNEL_PAGE_24_GHZ, channel)
.setChannelMask(channelMask)
.setPskc(newRandomBytes(secureRandom, LENGTH_PSKC))
.setNetworkKey(newRandomBytes(secureRandom, LENGTH_NETWORK_KEY))
@@ -641,6 +665,18 @@
.build();
}
+ private static int selectChannel(
+ int supportedChannelMask, int preferredChannelMask, Random random) {
+ // If the preferred channel mask is not empty, select a random channel from it, otherwise
+ // choose one from the supported channel mask.
+ preferredChannelMask = preferredChannelMask & supportedChannelMask;
+ if (preferredChannelMask == 0) {
+ preferredChannelMask = supportedChannelMask;
+ }
+
+ return selectRandomChannel(preferredChannelMask, random);
+ }
+
private static byte[] newRandomBytes(Random random, int length) {
byte[] result = new byte[length];
random.nextBytes(result);
@@ -740,9 +776,6 @@
return ERROR_ABORTED;
case OT_ERROR_BUSY:
return ERROR_BUSY;
- case OT_ERROR_DETACHED:
- case OT_ERROR_INVALID_STATE:
- return ERROR_FAILED_PRECONDITION;
case OT_ERROR_NO_BUFS:
return ERROR_RESOURCE_EXHAUSTED;
case OT_ERROR_PARSE:
@@ -756,6 +789,9 @@
return ERROR_UNSUPPORTED_CHANNEL;
case OT_ERROR_THREAD_DISABLED:
return ERROR_THREAD_DISABLED;
+ case OT_ERROR_FAILED_PRECONDITION:
+ return ERROR_FAILED_PRECONDITION;
+ case OT_ERROR_INVALID_STATE:
default:
return ERROR_INTERNAL_ERROR;
}
diff --git a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
index 2fccf6b..7554610 100644
--- a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
+++ b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
@@ -18,19 +18,22 @@
import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
import static android.Manifest.permission.NETWORK_SETTINGS;
-import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER;
import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
import static android.net.thread.utils.IntegrationTestUtils.JOIN_TIMEOUT;
import static android.net.thread.utils.IntegrationTestUtils.RESTART_JOIN_TIMEOUT;
import static android.net.thread.utils.IntegrationTestUtils.isExpectedIcmpv6Packet;
+import static android.net.thread.utils.IntegrationTestUtils.isFromIpv6Source;
+import static android.net.thread.utils.IntegrationTestUtils.isInMulticastGroup;
import static android.net.thread.utils.IntegrationTestUtils.isSimulatedThreadRadioSupported;
+import static android.net.thread.utils.IntegrationTestUtils.isToIpv6Destination;
import static android.net.thread.utils.IntegrationTestUtils.newPacketReader;
-import static android.net.thread.utils.IntegrationTestUtils.readPacketFrom;
+import static android.net.thread.utils.IntegrationTestUtils.pollForPacket;
import static android.net.thread.utils.IntegrationTestUtils.sendUdpMessage;
import static android.net.thread.utils.IntegrationTestUtils.waitFor;
-import static android.net.thread.utils.IntegrationTestUtils.waitForStateAnyOf;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REPLY_TYPE;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
+import static com.android.testutils.DeviceInfoUtils.isKernelVersionAtLeast;
import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
import static com.android.testutils.TestPermissionUtil.runAsShell;
@@ -38,13 +41,16 @@
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
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.Assume.assumeNotNull;
import static org.junit.Assume.assumeTrue;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.content.Context;
+import android.net.InetAddresses;
import android.net.LinkProperties;
import android.net.MacAddress;
import android.net.thread.utils.FullThreadDevice;
@@ -66,10 +72,12 @@
import java.net.Inet6Address;
import java.time.Duration;
+import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
/** Integration test cases for Thread Border Routing feature. */
@RunWith(AndroidJUnit4.class)
@@ -81,6 +89,18 @@
private HandlerThread mHandlerThread;
private Handler mHandler;
private TestNetworkTracker mInfraNetworkTracker;
+ private List<FullThreadDevice> mFtds;
+ private TapPacketReader mInfraNetworkReader;
+ private InfraNetworkDevice mInfraDevice;
+
+ private static final int NUM_FTD = 2;
+ private static final String KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED = "5.15.0";
+ private static final Inet6Address GROUP_ADDR_SCOPE_5 =
+ (Inet6Address) InetAddresses.parseNumericAddress("ff05::1234");
+ private static final Inet6Address GROUP_ADDR_SCOPE_4 =
+ (Inet6Address) InetAddresses.parseNumericAddress("ff04::1234");
+ private static final Inet6Address GROUP_ADDR_SCOPE_3 =
+ (Inet6Address) InetAddresses.parseNumericAddress("ff03::1234");
// A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
private static final byte[] DEFAULT_DATASET_TLVS =
@@ -95,6 +115,7 @@
@Before
public void setUp() throws Exception {
+ assumeTrue(isSimulatedThreadRadioSupported());
final ThreadNetworkManager manager = mContext.getSystemService(ThreadNetworkManager.class);
if (manager != null) {
mController = manager.getAllThreadNetworkControllers().get(0);
@@ -106,24 +127,21 @@
mHandlerThread = new HandlerThread(getClass().getSimpleName());
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
+ mFtds = new ArrayList<>();
- mInfraNetworkTracker =
- runAsShell(
- MANAGE_TEST_NETWORKS,
- () ->
- initTestNetwork(
- mContext, new LinkProperties(), 5000 /* timeoutMs */));
- runAsShell(
- PERMISSION_THREAD_NETWORK_PRIVILEGED,
- NETWORK_SETTINGS,
- () -> {
- CountDownLatch latch = new CountDownLatch(1);
- mController.setTestNetworkAsUpstream(
- mInfraNetworkTracker.getTestIface().getInterfaceName(),
- directExecutor(),
- v -> latch.countDown());
- latch.await();
- });
+ setUpInfraNetwork();
+
+ // BR forms a network.
+ startBrLeader();
+
+ // Creates a infra network device.
+ mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
+ startInfraDevice();
+
+ // Create Ftds
+ for (int i = 0; i < NUM_FTD; ++i) {
+ mFtds.add(new FullThreadDevice(15 + i /* node ID */));
+ }
}
@After
@@ -142,16 +160,19 @@
mController.leave(directExecutor(), v -> latch.countDown());
latch.await(10, TimeUnit.SECONDS);
});
- runAsShell(MANAGE_TEST_NETWORKS, () -> mInfraNetworkTracker.teardown());
+ tearDownInfraNetwork();
mHandlerThread.quitSafely();
mHandlerThread.join();
+
+ for (var ftd : mFtds) {
+ ftd.destroy();
+ }
+ mFtds.clear();
}
@Test
public void unicastRouting_infraDevicePingTheadDeviceOmr_replyReceived() throws Exception {
- assumeTrue(isSimulatedThreadRadioSupported());
-
/*
* <pre>
* Topology:
@@ -161,37 +182,15 @@
* </pre>
*/
- // BR forms a network.
- runAsShell(
- PERMISSION_THREAD_NETWORK_PRIVILEGED,
- () -> mController.join(DEFAULT_DATASET, directExecutor(), result -> {}));
- waitForStateAnyOf(mController, List.of(DEVICE_ROLE_LEADER), JOIN_TIMEOUT);
-
- // Creates a Full Thread Device (FTD) and lets it join the network.
- FullThreadDevice ftd = new FullThreadDevice(5 /* node ID */);
- ftd.factoryReset();
- ftd.joinNetwork(DEFAULT_DATASET);
- ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
- waitFor(() -> ftd.getOmrAddress() != null, Duration.ofSeconds(60));
- Inet6Address ftdOmr = ftd.getOmrAddress();
- assertNotNull(ftdOmr);
-
- // Creates a infra network device.
- TapPacketReader infraNetworkReader =
- newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
- InfraNetworkDevice infraDevice =
- new InfraNetworkDevice(MacAddress.fromString("1:2:3:4:5:6"), infraNetworkReader);
- infraDevice.runSlaac(Duration.ofSeconds(60));
- assertNotNull(infraDevice.ipv6Addr);
+ // Let ftd join the network.
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
// Infra device sends an echo request to FTD's OMR.
- infraDevice.sendEchoRequest(ftdOmr);
+ mInfraDevice.sendEchoRequest(ftd.getOmrAddress());
// Infra device receives an echo reply sent by FTD.
- assertNotNull(
- readPacketFrom(
- infraNetworkReader,
- p -> isExpectedIcmpv6Packet(p, ICMPV6_ECHO_REPLY_TYPE)));
+ assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, null /* srcAddress */));
}
@Test
@@ -216,12 +215,9 @@
joinFuture.get(RESTART_JOIN_TIMEOUT.toMillis(), MILLISECONDS);
// Creates a Full Thread Device (FTD) and lets it join the network.
- FullThreadDevice ftd = new FullThreadDevice(6 /* node ID */);
- ftd.joinNetwork(DEFAULT_DATASET);
- ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
- waitFor(() -> ftd.getOmrAddress() != null, Duration.ofSeconds(60));
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
Inet6Address ftdOmr = ftd.getOmrAddress();
- assertNotNull(ftdOmr);
Inet6Address ftdMlEid = ftd.getMlEid();
assertNotNull(ftdMlEid);
@@ -233,4 +229,422 @@
sendUdpMessage(ftdMlEid, 12345, "bbbbbbbb");
assertEquals("bbbbbbbb", ftd.udpReceive());
}
+
+ @Test
+ public void multicastRouting_ftdSubscribedMulticastAddress_infraLinkJoinsMulticastGroup()
+ throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+
+ ftd.subscribeMulticastAddress(GROUP_ADDR_SCOPE_5);
+
+ assertInfraLinkMemberOfGroup(GROUP_ADDR_SCOPE_5);
+ }
+
+ @Test
+ public void
+ multicastRouting_ftdSubscribedScope3MulticastAddress_infraLinkNotJoinMulticastGroup()
+ throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+
+ ftd.subscribeMulticastAddress(GROUP_ADDR_SCOPE_3);
+
+ assertInfraLinkNotMemberOfGroup(GROUP_ADDR_SCOPE_3);
+ }
+
+ @Test
+ public void multicastRouting_ftdSubscribedMulticastAddress_canPingfromInfraLink()
+ throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+ subscribeMulticastAddressAndWait(ftd, GROUP_ADDR_SCOPE_5);
+
+ mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
+
+ assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+ }
+
+ @Test
+ public void multicastRouting_ftdSubscribedScope3MulticastAddress_cannotPingfromInfraLink()
+ throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+ ftd.subscribeMulticastAddress(GROUP_ADDR_SCOPE_3);
+
+ mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_3);
+
+ assertNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+ }
+
+ @Test
+ public void multicastRouting_ftdNotSubscribedMulticastAddress_cannotPingFromInfraDevice()
+ throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+
+ mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_4);
+
+ assertNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+ }
+
+ @Test
+ public void multicastRouting_multipleFtdsSubscribedDifferentAddresses_canPingFromInfraDevice()
+ throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device 1
+ * (Cuttlefish)
+ * |
+ * | Thread
+ * |
+ * Full Thread device 2
+ * </pre>
+ */
+
+ FullThreadDevice ftd1 = mFtds.get(0);
+ startFtdChild(ftd1);
+ subscribeMulticastAddressAndWait(ftd1, GROUP_ADDR_SCOPE_5);
+
+ FullThreadDevice ftd2 = mFtds.get(1);
+ startFtdChild(ftd2);
+ subscribeMulticastAddressAndWait(ftd2, GROUP_ADDR_SCOPE_4);
+
+ mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
+ mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_4);
+
+ assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd1.getOmrAddress()));
+ assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd2.getOmrAddress()));
+ }
+
+ @Test
+ public void multicastRouting_multipleFtdsSubscribedSameAddress_canPingFromInfraDevice()
+ throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device 1
+ * (Cuttlefish)
+ * |
+ * | Thread
+ * |
+ * Full Thread device 2
+ * </pre>
+ */
+
+ FullThreadDevice ftd1 = mFtds.get(0);
+ startFtdChild(ftd1);
+ subscribeMulticastAddressAndWait(ftd1, GROUP_ADDR_SCOPE_5);
+
+ FullThreadDevice ftd2 = mFtds.get(1);
+ startFtdChild(ftd2);
+ subscribeMulticastAddressAndWait(ftd2, GROUP_ADDR_SCOPE_5);
+
+ // Send the request twice as the order of replies from ftd1 and ftd2 are not guaranteed
+ mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
+ mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
+
+ assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd1.getOmrAddress()));
+ assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd2.getOmrAddress()));
+ }
+
+ @Test
+ public void multicastRouting_outboundForwarding_scopeLargerThan3IsForwarded() throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+ Inet6Address ftdOmr = ftd.getOmrAddress();
+
+ ftd.ping(GROUP_ADDR_SCOPE_5);
+ ftd.ping(GROUP_ADDR_SCOPE_4);
+
+ assertNotNull(
+ pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_5));
+ assertNotNull(
+ pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_4));
+ }
+
+ @Test
+ public void multicastRouting_outboundForwarding_scopeSmallerThan4IsNotForwarded()
+ throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+
+ ftd.ping(GROUP_ADDR_SCOPE_3);
+
+ assertNull(
+ pollForPacketOnInfraNetwork(
+ ICMPV6_ECHO_REQUEST_TYPE, ftd.getOmrAddress(), GROUP_ADDR_SCOPE_3));
+ }
+
+ @Test
+ public void multicastRouting_outboundForwarding_llaToScope4IsNotForwarded() throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+ Inet6Address ftdLla = ftd.getLinkLocalAddress();
+ assertNotNull(ftdLla);
+
+ ftd.ping(GROUP_ADDR_SCOPE_4, ftdLla, 100 /* size */, 1 /* count */);
+
+ assertNull(
+ pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdLla, GROUP_ADDR_SCOPE_4));
+ }
+
+ @Test
+ public void multicastRouting_outboundForwarding_mlaToScope4IsNotForwarded() throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+ List<Inet6Address> ftdMlas = ftd.getMeshLocalAddresses();
+ assertFalse(ftdMlas.isEmpty());
+
+ for (Inet6Address ftdMla : ftdMlas) {
+ ftd.ping(GROUP_ADDR_SCOPE_4, ftdMla, 100 /* size */, 1 /* count */);
+
+ assertNull(
+ pollForPacketOnInfraNetwork(
+ ICMPV6_ECHO_REQUEST_TYPE, ftdMla, GROUP_ADDR_SCOPE_4));
+ }
+ }
+
+ @Test
+ public void multicastRouting_infraNetworkSwitch_ftdRepliesToSubscribedAddress()
+ throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+ subscribeMulticastAddressAndWait(ftd, GROUP_ADDR_SCOPE_5);
+ Inet6Address ftdOmr = ftd.getOmrAddress();
+
+ // Destroy infra link and re-create
+ tearDownInfraNetwork();
+ setUpInfraNetwork();
+ mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
+ startInfraDevice();
+
+ mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
+
+ assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftdOmr));
+ }
+
+ @Test
+ public void multicastRouting_infraNetworkSwitch_outboundPacketIsForwarded() throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+ Inet6Address ftdOmr = ftd.getOmrAddress();
+
+ // Destroy infra link and re-create
+ tearDownInfraNetwork();
+ setUpInfraNetwork();
+ mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
+ startInfraDevice();
+
+ ftd.ping(GROUP_ADDR_SCOPE_5);
+ ftd.ping(GROUP_ADDR_SCOPE_4);
+
+ assertNotNull(
+ pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_5));
+ assertNotNull(
+ pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_4));
+ }
+
+ private void setUpInfraNetwork() {
+ mInfraNetworkTracker =
+ runAsShell(
+ MANAGE_TEST_NETWORKS,
+ () ->
+ initTestNetwork(
+ mContext, new LinkProperties(), 5000 /* timeoutMs */));
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ NETWORK_SETTINGS,
+ () -> {
+ CompletableFuture<Void> future = new CompletableFuture<>();
+ mController.setTestNetworkAsUpstream(
+ mInfraNetworkTracker.getTestIface().getInterfaceName(),
+ directExecutor(),
+ future::complete);
+ future.get(5, TimeUnit.SECONDS);
+ });
+ }
+
+ private void tearDownInfraNetwork() {
+ runAsShell(MANAGE_TEST_NETWORKS, () -> mInfraNetworkTracker.teardown());
+ }
+
+ private void startBrLeader() throws Exception {
+ CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ () -> mController.join(DEFAULT_DATASET, directExecutor(), joinFuture::complete));
+ joinFuture.get(RESTART_JOIN_TIMEOUT.toSeconds(), TimeUnit.SECONDS);
+ }
+
+ private void startFtdChild(FullThreadDevice ftd) throws Exception {
+ ftd.factoryReset();
+ ftd.joinNetwork(DEFAULT_DATASET);
+ ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+ waitFor(() -> ftd.getOmrAddress() != null, Duration.ofSeconds(60));
+ Inet6Address ftdOmr = ftd.getOmrAddress();
+ assertNotNull(ftdOmr);
+ }
+
+ private void startInfraDevice() throws Exception {
+ mInfraDevice =
+ new InfraNetworkDevice(MacAddress.fromString("1:2:3:4:5:6"), mInfraNetworkReader);
+ mInfraDevice.runSlaac(Duration.ofSeconds(60));
+ assertNotNull(mInfraDevice.ipv6Addr);
+ }
+
+ private void assertInfraLinkMemberOfGroup(Inet6Address address) throws Exception {
+ waitFor(
+ () ->
+ isInMulticastGroup(
+ mInfraNetworkTracker.getTestIface().getInterfaceName(), address),
+ Duration.ofSeconds(3));
+ }
+
+ private void assertInfraLinkNotMemberOfGroup(Inet6Address address) throws Exception {
+ waitFor(
+ () ->
+ !isInMulticastGroup(
+ mInfraNetworkTracker.getTestIface().getInterfaceName(), address),
+ Duration.ofSeconds(3));
+ }
+
+ private void subscribeMulticastAddressAndWait(FullThreadDevice ftd, Inet6Address address)
+ throws Exception {
+ ftd.subscribeMulticastAddress(address);
+
+ assertInfraLinkMemberOfGroup(address);
+ }
+
+ private byte[] pollForPacketOnInfraNetwork(int type, Inet6Address srcAddress) {
+ return pollForPacketOnInfraNetwork(type, srcAddress, null);
+ }
+
+ private byte[] pollForPacketOnInfraNetwork(
+ int type, Inet6Address srcAddress, Inet6Address destAddress) {
+ Predicate<byte[]> filter;
+ filter =
+ p ->
+ (isExpectedIcmpv6Packet(p, type)
+ && (srcAddress == null ? true : isFromIpv6Source(p, srcAddress))
+ && (destAddress == null
+ ? true
+ : isToIpv6Destination(p, destAddress)));
+ return pollForPacket(mInfraNetworkReader, filter);
+ }
}
diff --git a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
index 5ca40e3..6cb1675 100644
--- a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
+++ b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
@@ -75,6 +75,10 @@
mActiveOperationalDataset = null;
}
+ public void destroy() {
+ mProcess.destroy();
+ }
+
/**
* Returns an OMR (Off-Mesh-Routable) address on this device if any.
*
@@ -103,6 +107,39 @@
}
/**
+ * Returns the link-local address of the device.
+ *
+ * <p>This methods goes through all unicast addresses on the device and returns the address that
+ * begins with fe80.
+ */
+ public Inet6Address getLinkLocalAddress() {
+ List<String> output = executeCommand("ipaddr linklocal");
+ if (!output.isEmpty() && output.get(0).startsWith("fe80:")) {
+ return (Inet6Address) InetAddresses.parseNumericAddress(output.get(0));
+ }
+ return null;
+ }
+
+ /**
+ * Returns the mesh-local addresses of the device.
+ *
+ * <p>This methods goes through all unicast addresses on the device and returns the address that
+ * begins with mesh-local prefix.
+ */
+ public List<Inet6Address> getMeshLocalAddresses() {
+ List<String> addresses = executeCommand("ipaddr");
+ List<Inet6Address> meshLocalAddresses = new ArrayList<>();
+ IpPrefix meshLocalPrefix = mActiveOperationalDataset.getMeshLocalPrefix();
+ for (String address : addresses) {
+ Inet6Address addr = (Inet6Address) InetAddresses.parseNumericAddress(address);
+ if (meshLocalPrefix.contains(addr)) {
+ meshLocalAddresses.add(addr);
+ }
+ }
+ return meshLocalAddresses;
+ }
+
+ /**
* Joins the Thread network using the given {@link ActiveOperationalDataset}.
*
* @param dataset the Active Operational Dataset
@@ -182,6 +219,27 @@
}
}
+ public void subscribeMulticastAddress(Inet6Address address) {
+ executeCommand("ipmaddr add " + address.getHostAddress());
+ }
+
+ public void ping(Inet6Address address, Inet6Address source, int size, int count) {
+ String cmd =
+ "ping"
+ + ((source == null) ? "" : (" -I " + source.getHostAddress()))
+ + " "
+ + address.getHostAddress()
+ + " "
+ + size
+ + " "
+ + count;
+ executeCommand(cmd);
+ }
+
+ public void ping(Inet6Address address) {
+ ping(address, null, 100 /* size */, 1 /* count */);
+ }
+
private List<String> executeCommand(String command) {
try {
mWriter.write(command + "\n");
diff --git a/thread/tests/integration/src/android/net/thread/utils/InfraNetworkDevice.java b/thread/tests/integration/src/android/net/thread/utils/InfraNetworkDevice.java
index 3081f9f..72a278c 100644
--- a/thread/tests/integration/src/android/net/thread/utils/InfraNetworkDevice.java
+++ b/thread/tests/integration/src/android/net/thread/utils/InfraNetworkDevice.java
@@ -16,7 +16,7 @@
package android.net.thread.utils;
import static android.net.thread.utils.IntegrationTestUtils.getRaPios;
-import static android.net.thread.utils.IntegrationTestUtils.readPacketFrom;
+import static android.net.thread.utils.IntegrationTestUtils.pollForPacket;
import static android.net.thread.utils.IntegrationTestUtils.waitFor;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA;
@@ -109,7 +109,7 @@
try {
sendRsPacket();
- final byte[] raPacket = readPacketFrom(packetReader, p -> !getRaPios(p).isEmpty());
+ final byte[] raPacket = pollForPacket(packetReader, p -> !getRaPios(p).isEmpty());
final List<PrefixInformationOption> options = getRaPios(raPacket);
diff --git a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
index 4eef0e5..74251a6 100644
--- a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
+++ b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
@@ -17,6 +17,7 @@
import static android.system.OsConstants.IPPROTO_ICMPV6;
+import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_PIO;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
@@ -42,6 +43,7 @@
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
+import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
@@ -149,17 +151,17 @@
}
/**
- * Reads a packet from a given {@link TapPacketReader} that satisfies the {@code filter}.
+ * Polls for a packet from a given {@link TapPacketReader} that satisfies the {@code filter}.
*
* @param packetReader a TUN packet reader
* @param filter the filter to be applied on the packet
* @return the first IPv6 packet that satisfies the {@code filter}. If it has waited for more
* than 3000ms to read the next packet, the method will return null
*/
- public static byte[] readPacketFrom(TapPacketReader packetReader, Predicate<byte[]> filter) {
+ public static byte[] pollForPacket(TapPacketReader packetReader, Predicate<byte[]> filter) {
byte[] packet;
- while ((packet = packetReader.poll(3000 /* timeoutMs */)) != null) {
- if (filter.test(packet)) return packet;
+ while ((packet = packetReader.poll(3000 /* timeoutMs */, filter)) != null) {
+ return packet;
}
return null;
}
@@ -182,6 +184,34 @@
return false;
}
+ public static boolean isFromIpv6Source(byte[] packet, Inet6Address src) {
+ if (packet == null) {
+ return false;
+ }
+ ByteBuffer buf = ByteBuffer.wrap(packet);
+ try {
+ return Struct.parse(Ipv6Header.class, buf).srcIp.equals(src);
+ } catch (IllegalArgumentException ignored) {
+ // It's fine that the passed in packet is malformed because it's could be sent
+ // by anybody.
+ }
+ return false;
+ }
+
+ public static boolean isToIpv6Destination(byte[] packet, Inet6Address dest) {
+ if (packet == null) {
+ return false;
+ }
+ ByteBuffer buf = ByteBuffer.wrap(packet);
+ try {
+ return Struct.parse(Ipv6Header.class, buf).dstIp.equals(dest);
+ } catch (IllegalArgumentException ignored) {
+ // It's fine that the passed in packet is malformed because it's could be sent
+ // by anybody.
+ }
+ return false;
+ }
+
/** Returns the Prefix Information Options (PIO) extracted from an ICMPv6 RA message. */
public static List<PrefixInformationOption> getRaPios(byte[] raMsg) {
final ArrayList<PrefixInformationOption> pioList = new ArrayList<>();
@@ -247,4 +277,16 @@
socket.send(packet);
}
}
+
+ public static boolean isInMulticastGroup(String interfaceName, Inet6Address address) {
+ final String cmd = "ip -6 maddr show dev " + interfaceName;
+ final String output = runShellCommandOrThrow(cmd);
+ final String addressStr = address.getHostAddress();
+ for (final String line : output.split("\\n")) {
+ if (line.contains(addressStr)) {
+ return true;
+ }
+ }
+ return false;
+ }
}
diff --git a/thread/tests/unit/AndroidTest.xml b/thread/tests/unit/AndroidTest.xml
index 26813c1..d16e423 100644
--- a/thread/tests/unit/AndroidTest.xml
+++ b/thread/tests/unit/AndroidTest.xml
@@ -19,6 +19,18 @@
<option name="test-tag" value="ThreadNetworkUnitTests" />
<option name="test-suite-tag" value="apct" />
+ <!--
+ Only run tests if the device under test is SDK version 34 (Android 14) or above.
+ -->
+ <object type="module_controller"
+ class="com.android.tradefed.testtype.suite.module.Sdk34ModuleController" />
+
+ <!-- Run tests in MTS only if the Tethering Mainline module is installed. -->
+ <object type="module_controller"
+ class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+ <option name="mainline-module-package-name" value="com.google.android.tethering" />
+ </object>
+
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="test-file-name" value="ThreadNetworkUnitTests.apk" />
<option name="check-min-sdk" value="true" />
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 1640679..4948c22 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -16,6 +16,8 @@
package com.android.server.thread;
+import static android.Manifest.permission.ACCESS_NETWORK_STATE;
+import static android.net.thread.ActiveOperationalDataset.CHANNEL_PAGE_24_GHZ;
import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
import static android.net.thread.ThreadNetworkException.ERROR_FAILED_PRECONDITION;
@@ -23,16 +25,19 @@
import static android.net.thread.ThreadNetworkManager.DISALLOW_THREAD_NETWORK;
import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
-import static com.android.testutils.TestPermissionUtil.runAsShell;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_INVALID_STATE;
import static com.google.common.io.BaseEncoding.base16;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
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.any;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
@@ -47,9 +52,11 @@
import android.net.NetworkAgent;
import android.net.NetworkProvider;
import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.IActiveOperationalDatasetReceiver;
import android.net.thread.IOperationReceiver;
import android.net.thread.ThreadNetworkException;
import android.os.Handler;
+import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.UserManager;
@@ -64,6 +71,8 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -95,6 +104,12 @@
+ "B9D351B40C0402A0FFF8");
private static final ActiveOperationalDataset DEFAULT_ACTIVE_DATASET =
ActiveOperationalDataset.fromThreadTlvs(DEFAULT_ACTIVE_DATASET_TLVS);
+ private static final String DEFAULT_NETWORK_NAME = "thread-wpan0";
+ private static final int OT_ERROR_NONE = 0;
+ private static final int DEFAULT_SUPPORTED_CHANNEL_MASK = 0x07FFF800; // from channel 11 to 26
+ private static final int DEFAULT_PREFERRED_CHANNEL_MASK = 0x00000800; // channel 11
+ private static final int DEFAULT_SELECTED_CHANNEL = 11;
+ private static final byte[] DEFAULT_SUPPORTED_CHANNEL_MASK_ARRAY = base16().decode("001FFFE0");
@Mock private ConnectivityManager mMockConnectivityManager;
@Mock private NetworkAgent mMockNetworkAgent;
@@ -104,16 +119,23 @@
@Mock private ThreadPersistentSettings mMockPersistentSettings;
@Mock private NsdPublisher mMockNsdPublisher;
@Mock private UserManager mMockUserManager;
+ @Mock private IBinder mIBinder;
private Context mContext;
private TestLooper mTestLooper;
private FakeOtDaemon mFakeOtDaemon;
private ThreadNetworkControllerService mService;
+ @Captor private ArgumentCaptor<ActiveOperationalDataset> mActiveDatasetCaptor;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mContext = spy(ApplicationProvider.getApplicationContext());
+ doNothing()
+ .when(mContext)
+ .enforceCallingOrSelfPermission(
+ eq(PERMISSION_THREAD_NETWORK_PRIVILEGED), anyString());
+
mTestLooper = new TestLooper();
final Handler handler = new Handler(mTestLooper.getLooper());
NetworkProvider networkProvider =
@@ -158,9 +180,7 @@
final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
mFakeOtDaemon.setJoinException(new RemoteException("ot-daemon join() throws"));
- runAsShell(
- PERMISSION_THREAD_NETWORK_PRIVILEGED,
- () -> mService.join(DEFAULT_ACTIVE_DATASET, mockReceiver));
+ mService.join(DEFAULT_ACTIVE_DATASET, mockReceiver);
mTestLooper.dispatchAll();
verify(mockReceiver, never()).onSuccess();
@@ -172,9 +192,7 @@
mService.initialize();
final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
- runAsShell(
- PERMISSION_THREAD_NETWORK_PRIVILEGED,
- () -> mService.join(DEFAULT_ACTIVE_DATASET, mockReceiver));
+ mService.join(DEFAULT_ACTIVE_DATASET, mockReceiver);
// Here needs to call Testlooper#dispatchAll twices because TestLooper#moveTimeForward
// operates on only currently enqueued messages but the delayed message is posted from
// another Handler task.
@@ -258,9 +276,7 @@
mService.initialize();
CompletableFuture<Void> setEnabledFuture = new CompletableFuture<>();
- runAsShell(
- PERMISSION_THREAD_NETWORK_PRIVILEGED,
- () -> mService.setEnabled(true, newOperationReceiver(setEnabledFuture)));
+ mService.setEnabled(true, newOperationReceiver(setEnabledFuture));
mTestLooper.dispatchAll();
var thrown = assertThrows(ExecutionException.class, () -> setEnabledFuture.get());
@@ -281,4 +297,40 @@
}
};
}
+
+ @Test
+ public void createRandomizedDataset_succeed_activeDatasetCreated() throws Exception {
+ final IActiveOperationalDatasetReceiver mockReceiver =
+ mock(IActiveOperationalDatasetReceiver.class);
+ mFakeOtDaemon.setChannelMasks(
+ DEFAULT_SUPPORTED_CHANNEL_MASK, DEFAULT_PREFERRED_CHANNEL_MASK);
+ mFakeOtDaemon.setChannelMasksReceiverOtError(OT_ERROR_NONE);
+
+ mService.createRandomizedDataset(DEFAULT_NETWORK_NAME, mockReceiver);
+ mTestLooper.dispatchAll();
+
+ verify(mockReceiver, never()).onError(anyInt(), anyString());
+ verify(mockReceiver, times(1)).onSuccess(mActiveDatasetCaptor.capture());
+ ActiveOperationalDataset activeDataset = mActiveDatasetCaptor.getValue();
+ assertThat(activeDataset.getNetworkName()).isEqualTo(DEFAULT_NETWORK_NAME);
+ assertThat(activeDataset.getChannelMask().size()).isEqualTo(1);
+ assertThat(activeDataset.getChannelMask().get(CHANNEL_PAGE_24_GHZ))
+ .isEqualTo(DEFAULT_SUPPORTED_CHANNEL_MASK_ARRAY);
+ assertThat(activeDataset.getChannel()).isEqualTo(DEFAULT_SELECTED_CHANNEL);
+ }
+
+ @Test
+ public void createRandomizedDataset_otDaemonRemoteFailure_returnsPreconditionError()
+ throws Exception {
+ final IActiveOperationalDatasetReceiver mockReceiver =
+ mock(IActiveOperationalDatasetReceiver.class);
+ mFakeOtDaemon.setChannelMasksReceiverOtError(OT_ERROR_INVALID_STATE);
+ when(mockReceiver.asBinder()).thenReturn(mIBinder);
+
+ mService.createRandomizedDataset(DEFAULT_NETWORK_NAME, mockReceiver);
+ mTestLooper.dispatchAll();
+
+ verify(mockReceiver, never()).onSuccess(any(ActiveOperationalDataset.class));
+ verify(mockReceiver, times(1)).onError(eq(ERROR_INTERNAL_ERROR), anyString());
+ }
}