diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java index 1001423732..48d40e61bd 100644 --- a/framework-t/src/android/net/nsd/NsdManager.java +++ b/framework-t/src/android/net/nsd/NsdManager.java @@ -389,6 +389,7 @@ public final class NsdManager { } private static final int FIRST_LISTENER_KEY = 1; + private static final int DNSSEC_PROTOCOL = 3; private final INsdServiceConnector mService; private final Context mContext; @@ -1754,45 +1755,132 @@ public final class NsdManager { } } + private enum ServiceValidationType { + NO_SERVICE, + HAS_SERVICE, // A service with a positive port + HAS_SERVICE_ZERO_PORT, // A service with a zero port + } + + private enum HostValidationType { + DEFAULT_HOST, // No host is specified so the default host will be used + CUSTOM_HOST, // A custom host with addresses is specified + CUSTOM_HOST_NO_ADDRESS, // A custom host without address is specified + } + + private enum PublicKeyValidationType { + NO_KEY, + HAS_KEY, + } + + /** + * Check if the service is valid for registration and classify it as one of {@link + * ServiceValidationType}. + */ + private static ServiceValidationType validateService(NsdServiceInfo serviceInfo) { + final boolean hasServiceName = !TextUtils.isEmpty(serviceInfo.getServiceName()); + final boolean hasServiceType = !TextUtils.isEmpty(serviceInfo.getServiceType()); + if (!hasServiceName && !hasServiceType && serviceInfo.getPort() == 0) { + return ServiceValidationType.NO_SERVICE; + } + if (hasServiceName && hasServiceType) { + if (serviceInfo.getPort() < 0) { + throw new IllegalArgumentException("Invalid port"); + } + if (serviceInfo.getPort() == 0) { + return ServiceValidationType.HAS_SERVICE_ZERO_PORT; + } + return ServiceValidationType.HAS_SERVICE; + } + throw new IllegalArgumentException("The service name or the service type is missing"); + } + + /** + * Check if the host is valid for registration and classify it as one of {@link + * HostValidationType}. + */ + private static HostValidationType validateHost(NsdServiceInfo serviceInfo) { + final boolean hasHostname = !TextUtils.isEmpty(serviceInfo.getHostname()); + final boolean hasHostAddresses = !CollectionUtils.isEmpty(serviceInfo.getHostAddresses()); + if (!hasHostname) { + // Keep compatible with the legacy behavior: It's allowed to set host + // addresses for a service registration although the host addresses + // won't be registered. To register the addresses for a host, the + // hostname must be specified. + return HostValidationType.DEFAULT_HOST; + } + if (!hasHostAddresses) { + return HostValidationType.CUSTOM_HOST_NO_ADDRESS; + } + return HostValidationType.CUSTOM_HOST; + } + + /** + * Check if the public key is valid for registration and classify it as one of {@link + * PublicKeyValidationType}. + * + *
For simplicity, it only checks if the protocol is DNSSEC and the RDATA is not fewer than 4 + * bytes. See RFC 3445 Section 3. + */ + private static PublicKeyValidationType validatePublicKey(NsdServiceInfo serviceInfo) { + byte[] publicKey = serviceInfo.getPublicKey(); + if (publicKey == null) { + return PublicKeyValidationType.NO_KEY; + } + if (publicKey.length < 4) { + throw new IllegalArgumentException("The public key should be at least 4 bytes long"); + } + int protocol = publicKey[2]; + if (protocol == DNSSEC_PROTOCOL) { + return PublicKeyValidationType.HAS_KEY; + } + throw new IllegalArgumentException( + "The public key's protocol (" + + protocol + + ") is invalid. It should be DNSSEC_PROTOCOL (3)"); + } + /** * Check if the {@link NsdServiceInfo} is valid for registration. * - * The following can be registered: - * - A service with an optional host. - * - A hostname with addresses. + *
Firstly, check if service, host and public key are all valid respectively. Then check if + * the combination of service, host and public key is valid. * - * Note that: - * - When registering a service, the service name, service type and port must be specified. If - * hostname is specified, the host addresses can optionally be specified. - * - When registering a host without a service, the addresses must be specified. + *
If the {@code serviceInfo} is invalid, throw an {@link IllegalArgumentException} + * describing the reason. + * + *
There are the invalid combinations of service, host and public key: + * + *
Keys are used to reserve hostnames or service names while the service/host is temporarily + * inactive, so registrations with a key and just a hostname or a service name are acceptable. * * @hide */ public static void checkServiceInfoForRegistration(NsdServiceInfo serviceInfo) { Objects.requireNonNull(serviceInfo, "NsdServiceInfo cannot be null"); - boolean hasServiceName = !TextUtils.isEmpty(serviceInfo.getServiceName()); - boolean hasServiceType = !TextUtils.isEmpty(serviceInfo.getServiceType()); - boolean hasHostname = !TextUtils.isEmpty(serviceInfo.getHostname()); - boolean hasHostAddresses = !CollectionUtils.isEmpty(serviceInfo.getHostAddresses()); - if (serviceInfo.getPort() < 0) { - throw new IllegalArgumentException("Invalid port"); + final ServiceValidationType serviceValidation = validateService(serviceInfo); + final HostValidationType hostValidation = validateHost(serviceInfo); + final PublicKeyValidationType publicKeyValidation = validatePublicKey(serviceInfo); + + if (serviceValidation == ServiceValidationType.NO_SERVICE + && hostValidation == HostValidationType.DEFAULT_HOST) { + throw new IllegalArgumentException("Nothing to register"); } - - if (hasServiceType || hasServiceName || (serviceInfo.getPort() > 0)) { - if (!(hasServiceType && hasServiceName && (serviceInfo.getPort() > 0))) { + if (publicKeyValidation == PublicKeyValidationType.NO_KEY) { + if (serviceValidation == ServiceValidationType.HAS_SERVICE_ZERO_PORT) { + throw new IllegalArgumentException("The port is missing"); + } + if (serviceValidation == ServiceValidationType.NO_SERVICE + && hostValidation == HostValidationType.CUSTOM_HOST_NO_ADDRESS) { throw new IllegalArgumentException( - "The service type, service name or port is missing"); + "The host addresses must be specified unless there is a service"); } - } - - if (!hasServiceType && !hasHostname) { - throw new IllegalArgumentException("No service or host specified in NsdServiceInfo"); - } - - if (!hasServiceType && hasHostname && !hasHostAddresses) { - // TODO: b/317946010 - This may be allowed when it supports registering KEY RR. - throw new IllegalArgumentException("No host addresses specified in NsdServiceInfo"); } } } diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java index 9491a9c5c0..2f675a9d0d 100644 --- a/framework-t/src/android/net/nsd/NsdServiceInfo.java +++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java @@ -37,6 +37,7 @@ import java.net.InetAddress; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; @@ -68,6 +69,9 @@ public final class NsdServiceInfo implements Parcelable { private int mPort; + @Nullable + private byte[] mPublicKey; + @Nullable private Network mNetwork; @@ -219,6 +223,40 @@ public final class NsdServiceInfo implements Parcelable { mHostname = hostname; } + /** + * Set the public key RDATA to be advertised in a KEY RR (RFC 2535). + * + *
This is the public key of the key pair used for signing a DNS message (e.g. SRP). Clients + * typically don't need this information, but the KEY RR is usually published to claim the use + * of the DNS name so that another mDNS advertiser can't take over the ownership during a + * temporary power down of the original host device. + * + *
When the public key is set to non-null, exactly one KEY RR will be advertised for each of
+ * the service and host name if they are not null.
+ *
+ * @hide // For Thread only
+ */
+ public void setPublicKey(@Nullable byte[] publicKey) {
+ if (publicKey == null) {
+ mPublicKey = null;
+ return;
+ }
+ mPublicKey = Arrays.copyOf(publicKey, publicKey.length);
+ }
+
+ /**
+ * Get the public key RDATA in the KEY RR (RFC 2535) or {@code null} if no KEY RR exists.
+ *
+ * @hide // For Thread only
+ */
+ @Nullable
+ public byte[] getPublicKey() {
+ if (mPublicKey == null) {
+ return null;
+ }
+ return Arrays.copyOf(mPublicKey, mPublicKey.length);
+ }
+
/**
* Unpack txt information from a base-64 encoded byte array.
*
@@ -622,6 +660,7 @@ public final class NsdServiceInfo implements Parcelable {
}
dest.writeString(mHostname);
dest.writeLong(mExpirationTime != null ? mExpirationTime.getEpochSecond() : -1);
+ dest.writeByteArray(mPublicKey);
}
/** Implement the Parcelable interface */
@@ -654,6 +693,7 @@ public final class NsdServiceInfo implements Parcelable {
info.mHostname = in.readString();
final long seconds = in.readLong();
info.setExpirationTime(seconds < 0 ? null : Instant.ofEpochSecond(seconds));
+ info.mPublicKey = in.createByteArray();
return info;
}
diff --git a/tests/common/java/android/net/nsd/NsdServiceInfoTest.java b/tests/common/java/android/net/nsd/NsdServiceInfoTest.java
index 8e89037724..21e34ab1ff 100644
--- a/tests/common/java/android/net/nsd/NsdServiceInfoTest.java
+++ b/tests/common/java/android/net/nsd/NsdServiceInfoTest.java
@@ -16,6 +16,7 @@
package android.net.nsd;
+import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
@@ -51,6 +52,23 @@ public class NsdServiceInfoTest {
private static final InetAddress IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1");
private static final InetAddress IPV6_ADDRESS = InetAddresses.parseNumericAddress("2001:db8::");
+ private static final byte[] PUBLIC_KEY_RDATA = new byte[] {
+ (byte) 0x02, (byte)0x01, // flag
+ (byte) 0x03, // protocol
+ (byte) 0x0d, // algorithm
+ // 64-byte public key below
+ (byte) 0xC1, (byte) 0x41, (byte) 0xD0, (byte) 0x63, (byte) 0x79, (byte) 0x60,
+ (byte) 0xB9, (byte) 0x8C, (byte) 0xBC, (byte) 0x12, (byte) 0xCF, (byte) 0xCA,
+ (byte) 0x22, (byte) 0x1D, (byte) 0x28, (byte) 0x79, (byte) 0xDA, (byte) 0xC2,
+ (byte) 0x6E, (byte) 0xE5, (byte) 0xB4, (byte) 0x60, (byte) 0xE9, (byte) 0x00,
+ (byte) 0x7C, (byte) 0x99, (byte) 0x2E, (byte) 0x19, (byte) 0x02, (byte) 0xD8,
+ (byte) 0x97, (byte) 0xC3, (byte) 0x91, (byte) 0xB0, (byte) 0x37, (byte) 0x64,
+ (byte) 0xD4, (byte) 0x48, (byte) 0xF7, (byte) 0xD0, (byte) 0xC7, (byte) 0x72,
+ (byte) 0xFD, (byte) 0xB0, (byte) 0x3B, (byte) 0x1D, (byte) 0x9D, (byte) 0x6D,
+ (byte) 0x52, (byte) 0xFF, (byte) 0x88, (byte) 0x86, (byte) 0x76, (byte) 0x9E,
+ (byte) 0x8E, (byte) 0x23, (byte) 0x62, (byte) 0x51, (byte) 0x35, (byte) 0x65,
+ (byte) 0x27, (byte) 0x09, (byte) 0x62, (byte) 0xD3
+ };
@Test
public void testLimits() throws Exception {
@@ -120,6 +138,7 @@ public class NsdServiceInfoTest {
fullInfo.setPort(4242);
fullInfo.setHostAddresses(List.of(IPV4_ADDRESS));
fullInfo.setHostname("home");
+ fullInfo.setPublicKey(PUBLIC_KEY_RDATA);
fullInfo.setNetwork(new Network(123));
fullInfo.setInterfaceIndex(456);
checkParcelable(fullInfo);
@@ -136,6 +155,7 @@ public class NsdServiceInfoTest {
attributedInfo.setPort(4242);
attributedInfo.setHostAddresses(List.of(IPV6_ADDRESS, IPV4_ADDRESS));
attributedInfo.setHostname("home");
+ attributedInfo.setPublicKey(PUBLIC_KEY_RDATA);
attributedInfo.setAttribute("color", "pink");
attributedInfo.setAttribute("sound", (new String("にゃあ")).getBytes("UTF-8"));
attributedInfo.setAttribute("adorable", (String) null);
@@ -172,6 +192,7 @@ public class NsdServiceInfoTest {
assertEquals(original.getServiceType(), result.getServiceType());
assertEquals(original.getHost(), result.getHost());
assertEquals(original.getHostname(), result.getHostname());
+ assertArrayEquals(original.getPublicKey(), result.getPublicKey());
assertTrue(original.getPort() == result.getPort());
assertEquals(original.getNetwork(), result.getNetwork());
assertEquals(original.getInterfaceIndex(), result.getInterfaceIndex());
diff --git a/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt b/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt
index 5ba6c4c720..93cec9cb7d 100644
--- a/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt
+++ b/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt
@@ -286,6 +286,12 @@ fun TapPacketReader.pollForQuery(
timeoutMs: Long = MDNS_REGISTRATION_TIMEOUT_MS
): TestDnsPacket? = pollForMdnsPacket(timeoutMs) { it.isQueryFor(recordName, *requiredTypes) }
+fun TapPacketReader.pollForReply(
+ recordName: String,
+ type: Int,
+ timeoutMs: Long = MDNS_REGISTRATION_TIMEOUT_MS
+): TestDnsPacket? = pollForMdnsPacket(timeoutMs) { it.isReplyFor(recordName, type) }
+
fun TapPacketReader.pollForReply(
serviceName: String,
serviceType: String,
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index 6dd4857496..6c6f6a338b 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -81,7 +81,9 @@ import com.android.compatibility.common.util.PropertyUtil
import com.android.compatibility.common.util.SystemUtil
import com.android.modules.utils.build.SdkLevel.isAtLeastU
import com.android.net.module.util.DnsPacket
+import com.android.net.module.util.DnsPacket.ANSECTION
import com.android.net.module.util.HexDump
+import com.android.net.module.util.HexDump.hexStringToByteArray
import com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN
import com.android.net.module.util.PacketBuilder
import com.android.testutils.ConnectivityModuleTest
@@ -96,6 +98,7 @@ import com.android.testutils.TapPacketReader
import com.android.testutils.TestableNetworkAgent
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnNetworkCreated
import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.assertContainsExactly
import com.android.testutils.assertEmpty
import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk30
import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk33
@@ -138,6 +141,9 @@ private const val REGISTRATION_TIMEOUT_MS = 10_000L
private const val DBG = false
private const val TEST_PORT = 12345
private const val MDNS_PORT = 5353.toShort()
+private const val TYPE_KEY = 25
+private const val QCLASS_INTERNET = 0x0001
+private const val NAME_RECORDS_TTL_MILLIS: Long = 120
private val multicastIpv6Addr = parseNumericAddress("ff02::fb") as Inet6Address
private val testSrcAddr = parseNumericAddress("2001:db8::123") as Inet6Address
@@ -167,6 +173,12 @@ class NsdManagerTest {
private val serviceType2 = "_nmt%09d._tcp".format(Random().nextInt(1_000_000_000))
private val customHostname = "NsdTestHost%09d".format(Random().nextInt(1_000_000_000))
private val customHostname2 = "NsdTestHost%09d".format(Random().nextInt(1_000_000_000))
+ private val publicKey = hexStringToByteArray(
+ "0201030dc141d0637960b98cbc12cfca"
+ + "221d2879dac26ee5b460e9007c992e19"
+ + "02d897c391b03764d448f7d0c772fdb0"
+ + "3b1d9d6d52ff8886769e8e2362513565"
+ + "270962d3")
private val handlerThread = HandlerThread(NsdManagerTest::class.java.simpleName)
private val ctsNetUtils by lazy{ CtsNetUtils(context) }
@@ -2265,6 +2277,165 @@ class NsdManagerTest {
}
}
+ @Test
+ fun testAdvertising_registerServiceAndPublicKey_keyAnnounced() {
+ val si = NsdServiceInfo().also {
+ it.network = testNetwork1.network
+ it.serviceType = serviceType
+ it.serviceName = serviceName
+ it.port = TEST_PORT
+ it.publicKey = publicKey
+ }
+ val packetReader = TapPacketReader(Handler(handlerThread.looper),
+ testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ packetReader.startAsyncForTest()
+ handlerThread.waitForIdle(TIMEOUT_MS)
+
+ val registrationRecord = NsdRegistrationRecord()
+ val discoveryRecord = NsdDiscoveryRecord()
+ tryTest {
+ registerService(registrationRecord, si)
+
+ val announcement = packetReader.pollForReply(
+ "$serviceName.$serviceType.local",
+ TYPE_KEY
+ )
+ assertNotNull(announcement)
+ val keyRecords = announcement.records[ANSECTION].filter { it.nsType == TYPE_KEY }
+ assertEquals(1, keyRecords.size)
+ val actualRecord = keyRecords.get(0)
+ assertEquals(TYPE_KEY, actualRecord.nsType)
+ assertEquals("$serviceName.$serviceType.local", actualRecord.dName)
+ assertEquals(NAME_RECORDS_TTL_MILLIS, actualRecord.ttl)
+ assertArrayEquals(publicKey, actualRecord.rr)
+
+ nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, discoveryRecord)
+
+ val discoveredInfo1 = discoveryRecord.waitForServiceDiscovered(
+ serviceName, serviceType, testNetwork1.network)
+ val resolvedInfo1 = resolveService(discoveredInfo1)
+
+ assertEquals(serviceName, discoveredInfo1.serviceName)
+ assertEquals(TEST_PORT, resolvedInfo1.port)
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(discoveryRecord)
+
+ discoveryRecord.expectCallback