Merge "Revert "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/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/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/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/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/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..b557e65 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,7 @@
package com.android.server.thread;
+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,6 +24,7 @@
import static android.net.thread.ThreadNetworkManager.DISALLOW_THREAD_NETWORK;
import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_INVALID_STATE;
import static com.android.testutils.TestPermissionUtil.runAsShell;
import static com.google.common.io.BaseEncoding.base16;
@@ -30,8 +32,10 @@
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.mock;
import static org.mockito.Mockito.never;
@@ -47,9 +51,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 +70,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 +103,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,10 +118,12 @@
@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() {
@@ -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());
+ }
}