Merge "Add UPDATE_CONFIG permission check when start CTJob" into main
diff --git a/framework/api/current.txt b/framework/api/current.txt
index 797c107..a9d1569 100644
--- a/framework/api/current.txt
+++ b/framework/api/current.txt
@@ -233,6 +233,32 @@
field @NonNull public static final android.os.Parcelable.Creator<android.net.IpPrefix> CREATOR;
}
+ @FlaggedApi("com.android.net.flags.ipv6_over_ble") public final class L2capNetworkSpecifier extends android.net.NetworkSpecifier implements android.os.Parcelable {
+ method public int describeContents();
+ method public int getHeaderCompression();
+ method public int getPsm();
+ method @Nullable public android.net.MacAddress getRemoteAddress();
+ method public int getRole();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.net.L2capNetworkSpecifier> CREATOR;
+ field public static final int HEADER_COMPRESSION_6LOWPAN = 2; // 0x2
+ field public static final int HEADER_COMPRESSION_ANY = 0; // 0x0
+ field public static final int HEADER_COMPRESSION_NONE = 1; // 0x1
+ field public static final int PSM_ANY = -1; // 0xffffffff
+ field public static final int ROLE_ANY = 0; // 0x0
+ field public static final int ROLE_CLIENT = 1; // 0x1
+ field public static final int ROLE_SERVER = 2; // 0x2
+ }
+
+ public static final class L2capNetworkSpecifier.Builder {
+ ctor public L2capNetworkSpecifier.Builder();
+ method @NonNull public android.net.L2capNetworkSpecifier build();
+ method @NonNull public android.net.L2capNetworkSpecifier.Builder setHeaderCompression(int);
+ method @NonNull public android.net.L2capNetworkSpecifier.Builder setPsm(int);
+ method @NonNull public android.net.L2capNetworkSpecifier.Builder setRemoteAddress(@NonNull android.net.MacAddress);
+ method @NonNull public android.net.L2capNetworkSpecifier.Builder setRole(int);
+ }
+
public class LinkAddress implements android.os.Parcelable {
method public int describeContents();
method public java.net.InetAddress getAddress();
diff --git a/framework/src/android/net/L2capNetworkSpecifier.java b/framework/src/android/net/L2capNetworkSpecifier.java
new file mode 100644
index 0000000..c7067f6
--- /dev/null
+++ b/framework/src/android/net/L2capNetworkSpecifier.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.net.flags.Flags;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * A {@link NetworkSpecifier} used to identify an L2CAP network.
+ *
+ * An L2CAP network is not symmetrical, meaning there exists both a server (Bluetooth peripheral)
+ * and a client (Bluetooth central) node. This specifier contains information required to request or
+ * reserve an L2CAP network.
+ *
+ * An L2CAP server network allocates a PSM to be advertised to the client. Therefore, the server
+ * network must always be reserved using {@link ConnectivityManager#reserveNetwork}. The subsequent
+ * {@link ConnectivityManager.NetworkCallback#onReserved(NetworkCapabilities)} includes information
+ * (i.e. the PSM) for the server to advertise to the client.
+ * Under the hood, an L2CAP server network is represented by a {@link
+ * android.bluetooth.BluetoothServerSocket} which can, in theory, accept many connections. However,
+ * before Android 15 Bluetooth APIs do not expose the channel ID, so these connections are
+ * indistinguishable. In practice, this means that network matching semantics in {@link
+ * ConnectivityService} will tear down all but the first connection.
+ *
+ * The L2cap client network can be connected using {@link ConnectivityManager#requestNetwork}
+ * including passing in the relevant information (i.e. PSM and destination MAC address) using the
+ * {@link L2capNetworkSpecifier}.
+ *
+ */
+@FlaggedApi(Flags.FLAG_IPV6_OVER_BLE)
+public final class L2capNetworkSpecifier extends NetworkSpecifier implements Parcelable {
+ /** Accept any role. */
+ public static final int ROLE_ANY = 0;
+ /** Specifier describes a client network. */
+ public static final int ROLE_CLIENT = 1;
+ /** Specifier describes a server network. */
+ public static final int ROLE_SERVER = 2;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = false, prefix = "ROLE_", value = {
+ ROLE_ANY,
+ ROLE_CLIENT,
+ ROLE_SERVER
+ })
+ public @interface Role {}
+ /** Role used to distinguish client from server networks. */
+ @Role
+ private final int mRole;
+
+ /** Accept any form of header compression. */
+ public static final int HEADER_COMPRESSION_ANY = 0;
+ /** Do not compress packets on this network. */
+ public static final int HEADER_COMPRESSION_NONE = 1;
+ /** Use 6lowpan header compression as specified in rfc6282. */
+ public static final int HEADER_COMPRESSION_6LOWPAN = 2;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = false, prefix = "HEADER_COMPRESSION_", value = {
+ HEADER_COMPRESSION_ANY,
+ HEADER_COMPRESSION_NONE,
+ HEADER_COMPRESSION_6LOWPAN
+ })
+ public @interface HeaderCompression {}
+ /** Header compression mechanism used on this network. */
+ @HeaderCompression
+ private final int mHeaderCompression;
+
+ /**
+ * The MAC address of the remote.
+ */
+ @Nullable
+ private final MacAddress mRemoteAddress;
+
+ /** Match any PSM. */
+ public static final int PSM_ANY = -1;
+
+ /** The Bluetooth L2CAP Protocol Service Multiplexer (PSM). */
+ private final int mPsm;
+
+ private L2capNetworkSpecifier(Parcel in) {
+ mRole = in.readInt();
+ mHeaderCompression = in.readInt();
+ mRemoteAddress = in.readParcelable(getClass().getClassLoader());
+ mPsm = in.readInt();
+ }
+
+ /** @hide */
+ public L2capNetworkSpecifier(@Role int role, @HeaderCompression int headerCompression,
+ MacAddress remoteAddress, int psm) {
+ mRole = role;
+ mHeaderCompression = headerCompression;
+ mRemoteAddress = remoteAddress;
+ mPsm = psm;
+ }
+
+ /** Returns the role to be used for this network. */
+ @Role
+ public int getRole() {
+ return mRole;
+ }
+
+ /** Returns the compression mechanism for this network. */
+ @HeaderCompression
+ public int getHeaderCompression() {
+ return mHeaderCompression;
+ }
+
+ /** Returns the remote MAC address for this network to connect to. */
+ public @Nullable MacAddress getRemoteAddress() {
+ return mRemoteAddress;
+ }
+
+ /** Returns the PSM for this network to connect to. */
+ public int getPsm() {
+ return mPsm;
+ }
+
+ /** A builder class for L2capNetworkSpecifier. */
+ public static final class Builder {
+ @Role
+ private int mRole;
+ @HeaderCompression
+ private int mHeaderCompression;
+ @Nullable
+ private MacAddress mRemoteAddress;
+ private int mPsm = PSM_ANY;
+
+ /**
+ * Set the role to use for this network.
+ *
+ * @param role the role to use.
+ */
+ @NonNull
+ public Builder setRole(@Role int role) {
+ mRole = role;
+ return this;
+ }
+
+ /**
+ * Set the header compression mechanism to use for this network.
+ *
+ * @param headerCompression the header compression mechanism to use.
+ */
+ @NonNull
+ public Builder setHeaderCompression(@HeaderCompression int headerCompression) {
+ mHeaderCompression = headerCompression;
+ return this;
+ }
+
+ /**
+ * Set the remote address for the client to connect to.
+ *
+ * Only valid for client networks. A null MacAddress matches *any* MacAddress.
+ *
+ * @param remoteAddress the MAC address to connect to.
+ */
+ @NonNull
+ public Builder setRemoteAddress(@NonNull MacAddress remoteAddress) {
+ Objects.requireNonNull(remoteAddress);
+ mRemoteAddress = remoteAddress;
+ return this;
+ }
+
+ /**
+ * Set the PSM for the client to connect to.
+ *
+ * Can only be configured on client networks.
+ *
+ * @param psm the Protocol Service Multiplexer (PSM) to connect to.
+ */
+ @NonNull
+ public Builder setPsm(int psm) {
+ mPsm = psm;
+ return this;
+ }
+
+ /** Create the L2capNetworkSpecifier object. */
+ @NonNull
+ public L2capNetworkSpecifier build() {
+ // TODO: throw an exception for combinations that cannot be supported.
+ return new L2capNetworkSpecifier(mRole, mHeaderCompression, mRemoteAddress, mPsm);
+ }
+ }
+
+ /** @hide */
+ @Override
+ public boolean canBeSatisfiedBy(NetworkSpecifier other) {
+ // TODO: implement matching semantics.
+ return false;
+ }
+
+ /** @hide */
+ @Override
+ @Nullable
+ public NetworkSpecifier redact() {
+ // Redact the remote MAC address and the PSM (for non-server roles).
+ final NetworkSpecifier redactedSpecifier = new Builder()
+ .setRole(mRole)
+ .setHeaderCompression(mHeaderCompression)
+ // TODO: consider not redacting the specifier in onReserved, so the redaction can be
+ // more strict (i.e. the PSM could always be redacted).
+ .setPsm(mRole == ROLE_SERVER ? mPsm : PSM_ANY)
+ .build();
+ return redactedSpecifier;
+ }
+
+ /** @hide */
+ @Override
+ public int hashCode() {
+ return Objects.hash(mRole, mHeaderCompression, mRemoteAddress, mPsm);
+ }
+
+ /** @hide */
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (!(obj instanceof L2capNetworkSpecifier)) return false;
+
+ final L2capNetworkSpecifier rhs = (L2capNetworkSpecifier) obj;
+ return mRole == rhs.mRole
+ && mHeaderCompression == rhs.mHeaderCompression
+ && Objects.equals(mRemoteAddress, rhs.mRemoteAddress)
+ && mPsm == rhs.mPsm;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mRole);
+ dest.writeInt(mHeaderCompression);
+ dest.writeParcelable(mRemoteAddress, flags);
+ dest.writeInt(mPsm);
+ }
+
+ public static final @NonNull Creator<L2capNetworkSpecifier> CREATOR = new Creator<>() {
+ @Override
+ public L2capNetworkSpecifier createFromParcel(Parcel in) {
+ return new L2capNetworkSpecifier(in);
+ }
+
+ @Override
+ public L2capNetworkSpecifier[] newArray(int size) {
+ return new L2capNetworkSpecifier[size];
+ }
+ };
+}
diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java
index b95363a..5a08d44 100644
--- a/framework/src/android/net/NetworkRequest.java
+++ b/framework/src/android/net/NetworkRequest.java
@@ -282,6 +282,13 @@
this.type = that.type;
}
+ private NetworkRequest(Parcel in) {
+ networkCapabilities = NetworkCapabilities.CREATOR.createFromParcel(in);
+ legacyType = in.readInt();
+ requestId = in.readInt();
+ type = Type.valueOf(in.readString()); // IllegalArgumentException if invalid.
+ }
+
/**
* Builder used to create {@link NetworkRequest} objects. Specify the Network features
* needed in terms of {@link NetworkCapabilities} features
@@ -678,12 +685,7 @@
public static final @android.annotation.NonNull Creator<NetworkRequest> CREATOR =
new Creator<NetworkRequest>() {
public NetworkRequest createFromParcel(Parcel in) {
- NetworkCapabilities nc = NetworkCapabilities.CREATOR.createFromParcel(in);
- int legacyType = in.readInt();
- int requestId = in.readInt();
- Type type = Type.valueOf(in.readString()); // IllegalArgumentException if invalid.
- NetworkRequest result = new NetworkRequest(nc, legacyType, requestId, type);
- return result;
+ return new NetworkRequest(in);
}
public NetworkRequest[] newArray(int size) {
return new NetworkRequest[size];
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt
index 8dc1bc4..bfbbc34 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt
@@ -14,19 +14,34 @@
* limitations under the License.
*/
-package com.android.testutils;
+package com.android.testutils
import android.content.Context
+import android.net.InetAddresses.parseNumericAddress
import android.net.KeepalivePacketData
+import android.net.LinkAddress
import android.net.LinkProperties
import android.net.NetworkAgent
import android.net.NetworkAgentConfig
import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
+import android.net.NetworkCapabilities.TRANSPORT_TEST
import android.net.NetworkProvider
+import android.net.NetworkRequest
import android.net.QosFilter
import android.net.Uri
import android.os.Looper
+import android.system.ErrnoException
+import android.system.Os
+import android.system.OsConstants
+import android.system.OsConstants.EADDRNOTAVAIL
+import android.system.OsConstants.ENETUNREACH
+import android.system.OsConstants.ENONET
+import android.system.OsConstants.IPPROTO_UDP
+import android.system.OsConstants.SOCK_DGRAM
+import com.android.modules.utils.build.SdkLevel.isAtLeastS
import com.android.net.module.util.ArrayTrackRecord
+import com.android.testutils.CompatUtil.makeTestNetworkSpecifier
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnAddKeepalivePacketFilter
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnAutomaticReconnectDisabled
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnBandwidthUpdateRequested
@@ -42,6 +57,8 @@
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnStopSocketKeepalive
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnUnregisterQosCallback
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnValidationStatus
+import java.net.NetworkInterface
+import java.net.SocketException
import java.time.Duration
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
@@ -65,6 +82,92 @@
conf: NetworkAgentConfig
) : NetworkAgent(context, looper, TestableNetworkAgent::class.java.simpleName /* tag */,
nc, lp, TEST_NETWORK_SCORE, conf, Provider(context, looper)) {
+ companion object {
+
+ /**
+ * Convenience method to create a [NetworkRequest] matching [TestableNetworkAgent]s from
+ * [createOnInterface].
+ */
+ fun makeNetworkRequestForInterface(ifaceName: String) = NetworkRequest.Builder()
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .addTransportType(TRANSPORT_TEST)
+ .setNetworkSpecifier(makeTestNetworkSpecifier(ifaceName))
+ .build()
+
+ /**
+ * Convenience method to initialize a [TestableNetworkAgent] on a given interface.
+ *
+ * This waits for link-local addresses to be setup and ensures LinkProperties are updated
+ * with the addresses.
+ */
+ fun createOnInterface(
+ context: Context,
+ looper: Looper,
+ ifaceName: String,
+ timeoutMs: Long
+ ): TestableNetworkAgent {
+ val lp = LinkProperties().apply {
+ interfaceName = ifaceName
+ }
+ val agent = TestableNetworkAgent(
+ context,
+ looper,
+ NetworkCapabilities().apply {
+ removeCapability(NET_CAPABILITY_TRUSTED)
+ addTransportType(TRANSPORT_TEST)
+ setNetworkSpecifier(makeTestNetworkSpecifier(ifaceName))
+ },
+ lp,
+ NetworkAgentConfig.Builder().build()
+ )
+ val network = agent.register()
+ agent.markConnected()
+ if (isAtLeastS()) {
+ // OnNetworkCreated was added in S
+ agent.eventuallyExpect<OnNetworkCreated>()
+ }
+
+ // Wait until the link-local address can be used. Address flags are not available
+ // without elevated permissions, so check that bindSocket works.
+ assertEventuallyTrue("No usable v6 address after $timeoutMs ms", timeoutMs) {
+ // To avoid race condition between socket connection succeeding and interface
+ // returning a non-empty address list. Verify that interface returns a non-empty
+ // list, before trying the socket connection.
+ if (NetworkInterface.getByName(ifaceName).interfaceAddresses.isEmpty()) {
+ return@assertEventuallyTrue false
+ }
+
+ val sock = Os.socket(OsConstants.AF_INET6, SOCK_DGRAM, IPPROTO_UDP)
+ tryTest {
+ network.bindSocket(sock)
+ Os.connect(sock, parseNumericAddress("ff02::fb%$ifaceName"), 12345)
+ true
+ }.catch<ErrnoException> {
+ if (it.errno != ENETUNREACH && it.errno != EADDRNOTAVAIL) {
+ throw it
+ }
+ false
+ }.catch<SocketException> {
+ // OnNetworkCreated does not exist on R, so a SocketException caused by ENONET
+ // may be seen before the network is created
+ if (isAtLeastS()) throw it
+ val cause = it.cause as? ErrnoException ?: throw it
+ if (cause.errno != ENONET) {
+ throw it
+ }
+ false
+ } cleanup {
+ Os.close(sock)
+ }
+ }
+
+ agent.lp.setLinkAddresses(NetworkInterface.getByName(ifaceName).interfaceAddresses.map {
+ LinkAddress(it.address, it.networkPrefixLength.toInt())
+ })
+ agent.sendLinkProperties(agent.lp)
+ return agent
+ }
+ }
val DEFAULT_TIMEOUT_MS = 5000L
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java
index ad98a29..ac60b0f 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java
@@ -25,6 +25,7 @@
import android.content.Context;
import android.net.TetheringInterface;
import android.net.cts.util.CtsTetheringUtils;
+import android.net.cts.util.CtsTetheringUtils.TestTetheringEventCallback;
import android.net.wifi.SoftApConfiguration;
import android.net.wifi.WifiSsid;
@@ -37,6 +38,7 @@
public class TetheringTest {
private CtsTetheringUtils mCtsTetheringUtils;
private TetheringHelperClient mTetheringHelperClient;
+ private TestTetheringEventCallback mTetheringEventCallback;
@Before
public void setUp() throws Exception {
@@ -44,11 +46,14 @@
mCtsTetheringUtils = new CtsTetheringUtils(targetContext);
mTetheringHelperClient = new TetheringHelperClient(targetContext);
mTetheringHelperClient.bind();
+ mTetheringEventCallback = mCtsTetheringUtils.registerTetheringEventCallback();
}
@After
public void tearDown() throws Exception {
mTetheringHelperClient.unbind();
+ mCtsTetheringUtils.unregisterTetheringEventCallback(mTetheringEventCallback);
+ mCtsTetheringUtils.stopAllTethering();
}
/**
@@ -57,24 +62,20 @@
*/
@Test
public void testSoftApConfigurationRedactedForOtherUids() throws Exception {
- final CtsTetheringUtils.TestTetheringEventCallback tetherEventCallback =
- mCtsTetheringUtils.registerTetheringEventCallback();
+ mTetheringEventCallback.assumeWifiTetheringSupported(
+ getInstrumentation().getTargetContext());
SoftApConfiguration softApConfig = new SoftApConfiguration.Builder()
.setWifiSsid(WifiSsid.fromBytes("This is an SSID!"
.getBytes(StandardCharsets.UTF_8))).build();
final TetheringInterface tetheringInterface =
- mCtsTetheringUtils.startWifiTethering(tetherEventCallback, softApConfig);
+ mCtsTetheringUtils.startWifiTethering(mTetheringEventCallback, softApConfig);
assertNotNull(tetheringInterface);
assertEquals(softApConfig, tetheringInterface.getSoftApConfiguration());
- try {
- TetheringInterface tetheringInterfaceForApp2 =
- mTetheringHelperClient.getTetheredWifiInterface();
- assertNotNull(tetheringInterfaceForApp2);
- assertNull(tetheringInterfaceForApp2.getSoftApConfiguration());
- assertEquals(
- tetheringInterface.getInterface(), tetheringInterfaceForApp2.getInterface());
- } finally {
- mCtsTetheringUtils.stopWifiTethering(tetherEventCallback);
- }
+ TetheringInterface tetheringInterfaceForApp2 =
+ mTetheringHelperClient.getTetheredWifiInterface();
+ assertNotNull(tetheringInterfaceForApp2);
+ assertNull(tetheringInterfaceForApp2.getSoftApConfiguration());
+ assertEquals(
+ tetheringInterface.getInterface(), tetheringInterfaceForApp2.getInterface());
}
}
diff --git a/tests/cts/net/src/android/net/cts/DnsResolverTapTest.kt b/tests/cts/net/src/android/net/cts/DnsResolverTapTest.kt
new file mode 100644
index 0000000..b1b6e0d
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/DnsResolverTapTest.kt
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts
+
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.Manifest.permission.READ_DEVICE_CONFIG
+import android.net.DnsResolver
+import android.net.InetAddresses.parseNumericAddress
+import android.net.IpPrefix
+import android.net.MacAddress
+import android.net.RouteInfo
+import android.os.CancellationSignal
+import android.os.HandlerThread
+import android.os.SystemClock
+import android.provider.DeviceConfig
+import android.provider.DeviceConfig.NAMESPACE_NETD_NATIVE
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN
+import com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN
+import com.android.net.module.util.NetworkStackConstants.UDP_HEADER_LEN
+import com.android.testutils.AutoReleaseNetworkCallbackRule
+import com.android.testutils.DeviceConfigRule
+import com.android.testutils.DnsResolverModuleTest
+import com.android.testutils.IPv6UdpFilter
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.RouterAdvertisementResponder
+import com.android.testutils.TapPacketReaderRule
+import com.android.testutils.TestableNetworkAgent
+import com.android.testutils.TestDnsPacket
+import com.android.testutils.com.android.testutils.SetFeatureFlagsRule
+import com.android.testutils.runAsShell
+import java.net.Inet6Address
+import java.net.InetAddress
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private val TEST_DNSSERVER_MAC = MacAddress.fromString("00:11:22:33:44:55")
+private val TAG = DnsResolverTapTest::class.java.simpleName
+private const val TEST_TIMEOUT_MS = 10_000L
+
+@DnsResolverModuleTest
+@RunWith(AndroidJUnit4::class)
+class DnsResolverTapTest {
+ private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
+ private val handlerThread = HandlerThread(TAG)
+
+ @get:Rule(order = 1)
+ val deviceConfigRule = DeviceConfigRule()
+
+ @get:Rule(order = 2)
+ val featureFlagsRule = SetFeatureFlagsRule(
+ setFlagsMethod = { name, enabled ->
+ val value = when (enabled) {
+ null -> null
+ true -> "1"
+ false -> "0"
+ }
+ deviceConfigRule.setConfig(NAMESPACE_NETD_NATIVE, name, value)
+ },
+ getFlagsMethod = {
+ runAsShell(READ_DEVICE_CONFIG) {
+ DeviceConfig.getInt(NAMESPACE_NETD_NATIVE, it, 0) == 1
+ }
+ }
+ )
+
+ @get:Rule(order = 3)
+ val packetReaderRule = TapPacketReaderRule()
+
+ @get:Rule(order = 4)
+ val cbRule = AutoReleaseNetworkCallbackRule()
+
+ private val ndResponder by lazy { RouterAdvertisementResponder(packetReaderRule.reader) }
+ private val dnsServerAddr by lazy {
+ parseNumericAddress("fe80::124%${packetReaderRule.iface.interfaceName}") as Inet6Address
+ }
+ private lateinit var agent: TestableNetworkAgent
+
+ @Before
+ fun setUp() {
+ handlerThread.start()
+ val interfaceName = packetReaderRule.iface.interfaceName
+ val cb = cbRule.requestNetwork(TestableNetworkAgent.makeNetworkRequestForInterface(
+ interfaceName))
+ agent = runAsShell(MANAGE_TEST_NETWORKS) {
+ TestableNetworkAgent.createOnInterface(context, handlerThread.looper,
+ interfaceName, TEST_TIMEOUT_MS)
+ }
+ ndResponder.addNeighborEntry(TEST_DNSSERVER_MAC, dnsServerAddr)
+ ndResponder.start()
+ agent.lp.apply {
+ addDnsServer(dnsServerAddr)
+ // A default route is needed for DnsResolver.java to send queries over IPv6
+ // (see usage of DnsUtils.haveIpv6).
+ addRoute(RouteInfo(IpPrefix("::/0"), null, null))
+ }
+ agent.sendLinkProperties(agent.lp)
+ cb.eventuallyExpect<LinkPropertiesChanged> { it.lp.dnsServers.isNotEmpty() }
+ }
+
+ @After
+ fun tearDown() {
+ ndResponder.stop()
+ if (::agent.isInitialized) {
+ agent.unregister()
+ }
+ handlerThread.quitSafely()
+ handlerThread.join()
+ }
+
+ private class DnsCallback : DnsResolver.Callback<List<InetAddress>> {
+ override fun onAnswer(answer: List<InetAddress>, rcode: Int) = Unit
+ override fun onError(error: DnsResolver.DnsException) = Unit
+ }
+
+ /**
+ * Run a cancellation test.
+ *
+ * @param domain Domain name to query
+ * @param waitTimeForNoRetryAfterCancellationMs If positive, cancel the query and wait for that
+ * delay to check no retry is sent.
+ * @return The duration it took to receive all expected replies.
+ */
+ fun doCancellationTest(domain: String, waitTimeForNoRetryAfterCancellationMs: Long): Long {
+ val cancellationSignal = CancellationSignal()
+ val dnsCb = DnsCallback()
+ val queryStart = SystemClock.elapsedRealtime()
+ DnsResolver.getInstance().query(
+ agent.network, domain, 0 /* flags */,
+ Runnable::run /* executor */, cancellationSignal, dnsCb
+ )
+
+ if (waitTimeForNoRetryAfterCancellationMs > 0) {
+ cancellationSignal.cancel()
+ }
+ // Filter for queries on UDP port 53 for the specified domain
+ val filter = IPv6UdpFilter(dstPort = 53).and {
+ TestDnsPacket(
+ it.copyOfRange(ETHER_HEADER_LEN + IPV6_HEADER_LEN + UDP_HEADER_LEN, it.size),
+ dstAddr = dnsServerAddr
+ ).isQueryFor(domain, DnsResolver.TYPE_AAAA)
+ }
+
+ val reader = packetReaderRule.reader
+ assertNotNull(reader.poll(TEST_TIMEOUT_MS, filter), "Original query not found")
+ if (waitTimeForNoRetryAfterCancellationMs > 0) {
+ assertNull(reader.poll(waitTimeForNoRetryAfterCancellationMs, filter),
+ "Expected no retry query")
+ } else {
+ assertNotNull(reader.poll(TEST_TIMEOUT_MS, filter), "Retry query not found")
+ }
+ return SystemClock.elapsedRealtime() - queryStart
+ }
+
+ @SetFeatureFlagsRule.FeatureFlag("no_retry_after_cancel", true)
+ @Test
+ fun testCancellation() {
+ val timeWithRetryWhenNotCancelled = doCancellationTest("test1.example.com",
+ waitTimeForNoRetryAfterCancellationMs = 0L)
+ doCancellationTest("test2.example.com",
+ waitTimeForNoRetryAfterCancellationMs = timeWithRetryWhenNotCancelled + 50L)
+ }
+}
\ No newline at end of file
diff --git a/tests/cts/net/src/android/net/cts/L2capNetworkSpecifierTest.kt b/tests/cts/net/src/android/net/cts/L2capNetworkSpecifierTest.kt
new file mode 100644
index 0000000..b593baf
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/L2capNetworkSpecifierTest.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts
+
+import android.net.L2capNetworkSpecifier
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_6LOWPAN
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_NONE
+import android.net.L2capNetworkSpecifier.ROLE_CLIENT
+import android.net.L2capNetworkSpecifier.ROLE_SERVER
+import android.net.MacAddress
+import android.os.Build
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.assertParcelingIsLossless
+import kotlin.test.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@ConnectivityModuleTest
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class L2capNetworkSpecifierTest {
+ @Test
+ fun testParcelUnparcel() {
+ val remoteMac = MacAddress.fromString("01:02:03:04:05:06")
+ val specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_CLIENT)
+ .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+ .setPsm(42)
+ .setRemoteAddress(remoteMac)
+ .build()
+ assertParcelingIsLossless(specifier)
+ }
+
+ @Test
+ fun testGetters() {
+ val remoteMac = MacAddress.fromString("11:22:33:44:55:66")
+ val specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(HEADER_COMPRESSION_NONE)
+ .setPsm(123)
+ .setRemoteAddress(remoteMac)
+ .build()
+ assertEquals(ROLE_SERVER, specifier.getRole())
+ assertEquals(HEADER_COMPRESSION_NONE, specifier.getHeaderCompression())
+ assertEquals(123, specifier.getPsm())
+ assertEquals(remoteMac, specifier.getRemoteAddress())
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/NetworkReservationTest.kt b/tests/cts/net/src/android/net/cts/NetworkReservationTest.kt
new file mode 100644
index 0000000..0b10ef6
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NetworkReservationTest.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts
+
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.Manifest.permission.NETWORK_SETTINGS
+import android.net.ConnectivityManager
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
+import android.net.NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS
+import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
+import android.net.NetworkCapabilities.TRANSPORT_TEST
+import android.net.NetworkProvider
+import android.net.NetworkRequest
+import android.net.NetworkScore
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Reserved
+import com.android.testutils.RecorderCallback.CallbackEntry.Unavailable
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.TestableNetworkOfferCallback
+import com.android.testutils.runAsShell
+import kotlin.test.assertEquals
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val TAG = "NetworkReservationTest"
+
+private val NETWORK_SCORE = NetworkScore.Builder().build()
+private val ETHERNET_CAPS = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_ETHERNET)
+ .addTransportType(TRANSPORT_TEST)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_CONGESTED)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .build()
+private val BLANKET_CAPS = NetworkCapabilities(ETHERNET_CAPS).apply {
+ reservationId = RES_ID_MATCH_ALL_RESERVATIONS
+}
+private val ETHERNET_REQUEST = NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_ETHERNET)
+ .addTransportType(TRANSPORT_TEST)
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .build()
+private const val TIMEOUT_MS = 5_000L
+private const val NO_CB_TIMEOUT_MS = 200L
+
+// TODO: integrate with CSNetworkReservationTest and move to common tests.
+@ConnectivityModuleTest
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class NetworkReservationTest {
+ private val context = InstrumentationRegistry.getInstrumentation().context
+ private val cm = context.getSystemService(ConnectivityManager::class.java)!!
+ private val handlerThread = HandlerThread("$TAG handler thread").apply { start() }
+ private val handler = Handler(handlerThread.looper)
+ private val provider = NetworkProvider(context, handlerThread.looper, TAG)
+
+ @Before
+ fun setUp() {
+ runAsShell(NETWORK_SETTINGS) {
+ cm.registerNetworkProvider(provider)
+ }
+ }
+
+ @After
+ fun tearDown() {
+ runAsShell(NETWORK_SETTINGS) {
+ // unregisterNetworkProvider unregisters all associated NetworkOffers.
+ cm.unregisterNetworkProvider(provider)
+ }
+ handlerThread.quitSafely()
+ handlerThread.join()
+ }
+
+ fun NetworkCapabilities.copyWithReservationId(resId: Int) = NetworkCapabilities(this).also {
+ it.reservationId = resId
+ }
+
+ @Test
+ fun testReserveNetwork() {
+ // register blanket offer
+ val blanketOffer = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ runAsShell(MANAGE_TEST_NETWORKS) {
+ provider.registerNetworkOffer(NETWORK_SCORE, BLANKET_CAPS, handler::post, blanketOffer)
+ }
+
+ val cb = TestableNetworkCallback()
+ cm.reserveNetwork(ETHERNET_REQUEST, handler, cb)
+
+ // validate the reservation matches the blanket offer.
+ val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+ val reservationId = reservationReq.networkCapabilities.reservationId
+
+ // bring up reserved reservation offer
+ val reservedCaps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+ val reservedOffer = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ runAsShell(MANAGE_TEST_NETWORKS) {
+ provider.registerNetworkOffer(NETWORK_SCORE, reservedCaps, handler::post, reservedOffer)
+ }
+
+ // validate onReserved was sent to the app
+ val appObservedCaps = cb.expect<Reserved>().caps
+ assertEquals(reservedCaps, appObservedCaps)
+
+ // validate the reservation matches the reserved offer.
+ reservedOffer.expectOnNetworkNeeded(reservedCaps)
+
+ // reserved offer goes away
+ provider.unregisterNetworkOffer(reservedOffer)
+ cb.expect<Unavailable>()
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index c981a1b..ee31f1a 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -22,14 +22,10 @@
import android.net.ConnectivityManager.NetworkCallback
import android.net.DnsResolver
import android.net.InetAddresses.parseNumericAddress
-import android.net.LinkAddress
-import android.net.LinkProperties
import android.net.LocalSocket
import android.net.LocalSocketAddress
import android.net.MacAddress
import android.net.Network
-import android.net.NetworkAgentConfig
-import android.net.NetworkCapabilities
import android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED
import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
@@ -53,16 +49,10 @@
import android.os.HandlerThread
import android.platform.test.annotations.AppModeFull
import android.provider.DeviceConfig.NAMESPACE_TETHERING
-import android.system.ErrnoException
-import android.system.Os
-import android.system.OsConstants.AF_INET6
-import android.system.OsConstants.EADDRNOTAVAIL
-import android.system.OsConstants.ENETUNREACH
import android.system.OsConstants.ETH_P_IPV6
import android.system.OsConstants.IPPROTO_IPV6
import android.system.OsConstants.IPPROTO_UDP
import android.system.OsConstants.RT_SCOPE_LINK
-import android.system.OsConstants.SOCK_DGRAM
import android.util.Log
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
@@ -106,7 +96,6 @@
import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
import com.android.testutils.TestDnsPacket
import com.android.testutils.TestableNetworkAgent
-import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnNetworkCreated
import com.android.testutils.TestableNetworkCallback
import com.android.testutils.assertEmpty
import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk30
@@ -247,16 +236,12 @@
val tnm = context.getSystemService(TestNetworkManager::class.java)!!
val iface = tnm.createTapInterface()
val cb = TestableNetworkCallback()
- val testNetworkSpecifier = TestNetworkSpecifier(iface.interfaceName)
cm.requestNetwork(
- NetworkRequest.Builder()
- .removeCapability(NET_CAPABILITY_TRUSTED)
- .addTransportType(TRANSPORT_TEST)
- .setNetworkSpecifier(testNetworkSpecifier)
- .build(),
+ TestableNetworkAgent.makeNetworkRequestForInterface(iface.interfaceName),
cb
)
- val agent = registerTestNetworkAgent(iface.interfaceName)
+ val agent = TestableNetworkAgent.createOnInterface(context, handlerThread.looper,
+ iface.interfaceName, TIMEOUT_MS)
val network = agent.network ?: fail("Registered agent should have a network")
cb.eventuallyExpect<LinkPropertiesChanged>(TIMEOUT_MS) {
@@ -271,57 +256,6 @@
return TestTapNetwork(iface, cb, agent, network)
}
- private fun registerTestNetworkAgent(ifaceName: String): TestableNetworkAgent {
- val lp = LinkProperties().apply {
- interfaceName = ifaceName
- }
- val agent = TestableNetworkAgent(
- context,
- handlerThread.looper,
- NetworkCapabilities().apply {
- removeCapability(NET_CAPABILITY_TRUSTED)
- addTransportType(TRANSPORT_TEST)
- setNetworkSpecifier(TestNetworkSpecifier(ifaceName))
- },
- lp,
- NetworkAgentConfig.Builder().build()
- )
- val network = agent.register()
- agent.markConnected()
- agent.expectCallback<OnNetworkCreated>()
-
- // Wait until the link-local address can be used. Address flags are not available without
- // elevated permissions, so check that bindSocket works.
- PollingCheck.check("No usable v6 address on interface after $TIMEOUT_MS ms", TIMEOUT_MS) {
- // To avoid race condition between socket connection succeeding and interface returning
- // a non-empty address list. Verify that interface returns a non-empty list, before
- // trying the socket connection.
- if (NetworkInterface.getByName(ifaceName).interfaceAddresses.isEmpty()) {
- return@check false
- }
-
- val sock = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP)
- tryTest {
- network.bindSocket(sock)
- Os.connect(sock, parseNumericAddress("ff02::fb%$ifaceName"), 12345)
- true
- }.catch<ErrnoException> {
- if (it.errno != ENETUNREACH && it.errno != EADDRNOTAVAIL) {
- throw it
- }
- false
- } cleanup {
- Os.close(sock)
- }
- }
-
- lp.setLinkAddresses(NetworkInterface.getByName(ifaceName).interfaceAddresses.map {
- LinkAddress(it.address, it.networkPrefixLength.toInt())
- })
- agent.sendLinkProperties(lp)
- return agent
- }
-
private fun makeTestServiceInfo(network: Network? = null) = NsdServiceInfo().also {
it.serviceType = serviceType
it.serviceName = serviceName
@@ -576,7 +510,9 @@
assertEquals(testNetwork1.network, serviceLost.serviceInfo.network)
val newAgent = runAsShell(MANAGE_TEST_NETWORKS) {
- registerTestNetworkAgent(testNetwork1.iface.interfaceName)
+ TestableNetworkAgent.createOnInterface(context, handlerThread.looper,
+ testNetwork1.iface.interfaceName,
+ TIMEOUT_MS)
}
val newNetwork = newAgent.network ?: fail("Registered agent should have a network")
val serviceDiscovered3 = discoveryRecord.expectCallback<ServiceFound>()