From 52497d66ccdc04420e73b50737e8382bc6d21963 Mon Sep 17 00:00:00 2001 From: Handa Wang Date: Mon, 8 Jan 2024 05:56:30 +0000 Subject: [PATCH] [mdns] service implementation for public key Bug: 317946010 Bug: 333232582 Change-Id: Iab66bf0a1af14878c7b18a7a3015c2de7a6514c8 --- .../src/com/android/server/NsdService.java | 31 ++ .../connectivity/mdns/MdnsKeyRecord.java | 100 +++++ .../server/connectivity/mdns/MdnsPacket.java | 9 + .../server/connectivity/mdns/MdnsRecord.java | 1 + .../mdns/MdnsRecordRepository.java | 158 ++++++-- .../mdns/MdnsResponseErrorCode.java | 1 + .../net/src/android/net/cts/NsdManagerTest.kt | 6 +- .../mdns/MdnsRecordRepositoryTest.kt | 348 +++++++++++++++++- .../connectivity/mdns/MdnsRecordTests.java | 90 +++++ 9 files changed, 695 insertions(+), 49 deletions(-) create mode 100644 service-t/src/com/android/server/connectivity/mdns/MdnsKeyRecord.java diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java index 7c1ca309d6..0a8adf0f65 100644 --- a/service-t/src/com/android/server/NsdService.java +++ b/service-t/src/com/android/server/NsdService.java @@ -201,6 +201,7 @@ public class NsdService extends INsdManager.Stub { private static final int NO_SENT_QUERY_COUNT = 0; private static final int DISCOVERY_QUERY_SENT_CALLBACK = 1000; private static final int MAX_SUBTYPE_COUNT = 100; + private static final int DNSSEC_PROTOCOL = 3; private static final SharedLog LOGGER = new SharedLog("serviceDiscovery"); private final Context mContext; @@ -1009,6 +1010,17 @@ public class NsdService extends INsdManager.Stub { break; } + if (!checkPublicKey(serviceInfo.getPublicKey())) { + Log.e(TAG, + "Invalid public key: " + + Arrays.toString(serviceInfo.getPublicKey())); + clientInfo.onRegisterServiceFailedImmediately( + clientRequestId, + NsdManager.FAILURE_BAD_PARAMETERS, + false /* isLegacy */); + break; + } + Set subtypes = new ArraySet<>(serviceInfo.getSubtypes()); if (typeSubtype != null && typeSubtype.second != null) { for (String subType : typeSubtype.second) { @@ -1842,6 +1854,25 @@ public class NsdService extends INsdManager.Stub { return Pattern.compile(HOSTNAME_REGEX).matcher(hostname).matches(); } + /** + * Checks if the public key is valid. + * + *

For simplicity, it only checks if the protocol is DNSSEC and the RDATA is not fewer than 4 + * bytes. See RFC 3445 Section 3. + * + *

Message format: flags (2 bytes), protocol (1 byte), algorithm (1 byte), public key. + */ + private static boolean checkPublicKey(@Nullable byte[] publicKey) { + if (publicKey == null) { + return true; + } + if (publicKey.length < 4) { + return false; + } + int protocol = publicKey[2]; + return protocol == DNSSEC_PROTOCOL; + } + /** Returns {@code true} if {@code subtype} is a valid DNS-SD subtype label. */ private static boolean checkSubtypeLabel(String subtype) { return Pattern.compile("^" + SUBTYPE_LABEL_REGEX + "$").matcher(subtype).matches(); diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsKeyRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsKeyRecord.java new file mode 100644 index 0000000000..ba8a56e726 --- /dev/null +++ b/service-t/src/com/android/server/connectivity/mdns/MdnsKeyRecord.java @@ -0,0 +1,100 @@ +/* + * 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.connectivity.mdns; + +import static com.android.net.module.util.HexDump.toHexString; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +import androidx.annotation.VisibleForTesting; + +import java.io.IOException; +import java.util.Arrays; + +/** An mDNS "KEY" record, which contains a public key for a name. See RFC 2535. */ +@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) +public class MdnsKeyRecord extends MdnsRecord { + @Nullable private byte[] rData; + + public MdnsKeyRecord(@NonNull String[] name, @NonNull MdnsPacketReader reader) + throws IOException { + this(name, reader, false); + } + + public MdnsKeyRecord(@NonNull String[] name, @NonNull MdnsPacketReader reader, + boolean isQuestion) throws IOException { + super(name, TYPE_KEY, reader, isQuestion); + } + + public MdnsKeyRecord(@NonNull String[] name, boolean isUnicast) { + super(name, TYPE_KEY, + MdnsConstants.QCLASS_INTERNET | (isUnicast ? MdnsConstants.QCLASS_UNICAST : 0), + 0L /* receiptTimeMillis */, false /* cacheFlush */, 0L /* ttlMillis */); + } + + public MdnsKeyRecord(@NonNull String[] name, long receiptTimeMillis, boolean cacheFlush, + long ttlMillis, @Nullable byte[] rData) { + super(name, TYPE_KEY, MdnsConstants.QCLASS_INTERNET, receiptTimeMillis, cacheFlush, + ttlMillis); + if (rData != null) { + this.rData = Arrays.copyOf(rData, rData.length); + } + } + /** Returns the KEY RDATA in bytes **/ + public byte[] getRData() { + if (rData == null) { + return null; + } + return Arrays.copyOf(rData, rData.length); + } + + @Override + protected void readData(MdnsPacketReader reader) throws IOException { + rData = new byte[reader.getRemaining()]; + reader.readBytes(rData); + } + + @Override + protected void writeData(MdnsPacketWriter writer) throws IOException { + if (rData != null) { + writer.writeBytes(rData); + } + } + + @Override + public String toString() { + return "KEY: " + toHexString(rData); + } + + @Override + public int hashCode() { + return (super.hashCode() * 31) + Arrays.hashCode(rData); + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof MdnsKeyRecord)) { + return false; + } + + return super.equals(other) && Arrays.equals(rData, ((MdnsKeyRecord) other).rData); + } +} \ No newline at end of file diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsPacket.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPacket.java index 83ecabc3d8..aef821150d 100644 --- a/service-t/src/com/android/server/connectivity/mdns/MdnsPacket.java +++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPacket.java @@ -196,6 +196,15 @@ public class MdnsPacket { } } + case MdnsRecord.TYPE_KEY: { + try { + return new MdnsKeyRecord(name, reader, isQuestion); + } catch (IOException e) { + throw new ParseException(MdnsResponseErrorCode.ERROR_READING_KEY_RDATA, + "Failed to read KEY record from mDNS response.", e); + } + } + case MdnsRecord.TYPE_NSEC: { try { return new MdnsNsecRecord(name, reader, isQuestion); diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java index 1f9f42bcb6..b865319899 100644 --- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java +++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java @@ -41,6 +41,7 @@ public abstract class MdnsRecord { public static final int TYPE_PTR = 0x000C; public static final int TYPE_SRV = 0x0021; public static final int TYPE_TXT = 0x0010; + public static final int TYPE_KEY = 0x0019; public static final int TYPE_NSEC = 0x002f; public static final int TYPE_ANY = 0x00ff; 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 eb85110e7d..39e8bcca54 100644 --- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java +++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java @@ -184,6 +184,10 @@ public class MdnsRecordRepository { public final RecordInfo srvRecord; @Nullable public final RecordInfo txtRecord; + @Nullable + public final RecordInfo serviceKeyRecord; + @Nullable + public final RecordInfo hostKeyRecord; @NonNull public final List> addressRecords; @NonNull @@ -245,7 +249,6 @@ public class MdnsRecordRepository { nameRecordsTtlMillis = DEFAULT_NAME_RECORDS_TTL_MILLIS; } - final boolean hasService = !TextUtils.isEmpty(serviceInfo.getServiceType()); final boolean hasCustomHost = !TextUtils.isEmpty(serviceInfo.getHostname()); final String[] hostname = hasCustomHost @@ -253,9 +256,11 @@ public class MdnsRecordRepository { : deviceHostname; final ArrayList> allRecords = new ArrayList<>(5); - if (hasService) { - final String[] serviceType = splitServiceType(serviceInfo); - final String[] serviceName = splitFullyQualifiedName(serviceInfo, serviceType); + final boolean hasService = !TextUtils.isEmpty(serviceInfo.getServiceType()); + final String[] serviceType = hasService ? splitServiceType(serviceInfo) : null; + final String[] serviceName = + hasService ? splitFullyQualifiedName(serviceInfo, serviceType) : null; + if (hasService && hasSrvRecord(serviceInfo)) { // Service PTR records ptrRecords = new ArrayList<>(serviceInfo.getSubtypes().size() + 1); ptrRecords.add(new RecordInfo<>( @@ -336,6 +341,36 @@ public class MdnsRecordRepository { addressRecords = Collections.emptyList(); } + final boolean hasKey = hasKeyRecord(serviceInfo); + if (hasKey && hasService) { + this.serviceKeyRecord = new RecordInfo<>( + serviceInfo, + new MdnsKeyRecord( + serviceName, + 0L /*receiptTimeMillis */, + true /* cacheFlush */, + nameRecordsTtlMillis, + serviceInfo.getPublicKey()), + false /* sharedName */); + allRecords.add(this.serviceKeyRecord); + } else { + this.serviceKeyRecord = null; + } + if (hasKey && hasCustomHost) { + this.hostKeyRecord = new RecordInfo<>( + serviceInfo, + new MdnsKeyRecord( + hostname, + 0L /*receiptTimeMillis */, + true /* cacheFlush */, + nameRecordsTtlMillis, + serviceInfo.getPublicKey()), + false /* sharedName */); + allRecords.add(this.hostKeyRecord); + } else { + this.hostKeyRecord = null; + } + this.allRecords = Collections.unmodifiableList(allRecords); this.repliedServiceCount = repliedServiceCount; this.sentPacketCount = sentPacketCount; @@ -486,6 +521,22 @@ public class MdnsRecordRepository { ? inetAddressRecord.getInet6Address() : inetAddressRecord.getInet4Address())); } + + List keyRecords = new ArrayList<>(); + if (registration.serviceKeyRecord != null) { + keyRecords.add(registration.serviceKeyRecord.record); + } + if (registration.hostKeyRecord != null) { + keyRecords.add(registration.hostKeyRecord.record); + } + for (MdnsKeyRecord keyRecord : keyRecords) { + probingRecords.add(new MdnsKeyRecord( + keyRecord.getName(), + 0L /* receiptTimeMillis */, + false /* cacheFlush */, + keyRecord.getTtl(), + keyRecord.getRData())); + } return new MdnsProber.ProbingInfo(serviceId, probingRecords); } @@ -1101,18 +1152,15 @@ public class MdnsRecordRepository { Collections.emptyList() /* additionalRecords */); } - /** Check if the record is in any service registration */ - private boolean hasInetAddressRecord(@NonNull MdnsInetAddressRecord record) { - for (int i = 0; i < mServices.size(); i++) { - final ServiceRegistration registration = mServices.valueAt(i); - if (registration.exiting) continue; - - for (RecordInfo localRecord : registration.addressRecords) { - if (Objects.equals(localRecord.record, record)) { - return true; - } + /** Check if the record is in a registration */ + private static boolean hasInetAddressRecord( + @NonNull ServiceRegistration registration, @NonNull MdnsInetAddressRecord record) { + for (RecordInfo localRecord : registration.addressRecords) { + if (Objects.equals(localRecord.record, record)) { + return true; } } + return false; } @@ -1155,36 +1203,33 @@ public class MdnsRecordRepository { return conflicting; } - private static boolean conflictForService( @NonNull MdnsRecord record, @NonNull ServiceRegistration registration) { - if (registration.srvRecord == null) { + String[] fullServiceName; + if (registration.srvRecord != null) { + fullServiceName = registration.srvRecord.record.getName(); + } else if (registration.serviceKeyRecord != null) { + fullServiceName = registration.serviceKeyRecord.record.getName(); + } else { return false; } - final RecordInfo srvRecord = registration.srvRecord; - if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(record.getName(), srvRecord.record.getName())) { + if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(record.getName(), fullServiceName)) { return false; } // As per RFC6762 9., it's fine if the "conflict" is an identical record with same // data. - if (record instanceof MdnsServiceRecord) { - final MdnsServiceRecord local = srvRecord.record; - final MdnsServiceRecord other = (MdnsServiceRecord) record; - // Note "equals" does not consider TTL or receipt time, as intended here - if (Objects.equals(local, other)) { - return false; - } + if (record instanceof MdnsServiceRecord && equals(record, registration.srvRecord)) { + return false; + } + if (record instanceof MdnsTextRecord && equals(record, registration.txtRecord)) { + return false; + } + if (record instanceof MdnsKeyRecord && equals(record, registration.serviceKeyRecord)) { + return false; } - if (record instanceof MdnsTextRecord) { - final MdnsTextRecord local = registration.txtRecord.record; - final MdnsTextRecord other = (MdnsTextRecord) record; - if (Objects.equals(local, other)) { - return false; - } - } return true; } @@ -1196,6 +1241,11 @@ public class MdnsRecordRepository { return false; } + // It cannot be a hostname conflict because not record is registered with the hostname. + if (registration.addressRecords.isEmpty() && registration.hostKeyRecord == null) { + return false; + } + // The record's name cannot be registered by NsdManager so it's not a conflict. if (record.getName().length != 2 || !record.getName()[1].equals(LOCAL_TLD)) { return false; @@ -1207,13 +1257,26 @@ public class MdnsRecordRepository { return false; } - // If this registration has any address record and there's no identical record in the - // repository, it's a conflict. There will be no conflict if no registration has addresses - // for that hostname. - if (record instanceof MdnsInetAddressRecord) { - if (!registration.addressRecords.isEmpty()) { - return !hasInetAddressRecord((MdnsInetAddressRecord) record); - } + // As per RFC6762 9., it's fine if the "conflict" is an identical record with same + // data. + if (record instanceof MdnsInetAddressRecord + && hasInetAddressRecord(registration, (MdnsInetAddressRecord) record)) { + return false; + } + if (record instanceof MdnsKeyRecord && equals(record, registration.hostKeyRecord)) { + return false; + } + + // Per RFC 6762 8.1, when a record is being probed, any answer containing a record with that + // name, of any type, MUST be considered a conflicting response. + if (registration.isProbing) { + return true; + } + if (record instanceof MdnsInetAddressRecord && !registration.addressRecords.isEmpty()) { + return true; + } + if (record instanceof MdnsKeyRecord && registration.hostKeyRecord != null) { + return true; } return false; @@ -1402,4 +1465,21 @@ public class MdnsRecordRepository { return type; } + + /** Returns whether there will be an SRV record when registering the {@code info}. */ + private static boolean hasSrvRecord(@NonNull NsdServiceInfo info) { + return info.getPort() > 0; + } + + /** Returns whether there will be KEY record(s) when registering the {@code info}. */ + private static boolean hasKeyRecord(@NonNull NsdServiceInfo info) { + return info.getPublicKey() != null; + } + + private static boolean equals(@NonNull MdnsRecord record, @Nullable RecordInfo recordInfo) { + if (recordInfo == null) { + return false; + } + return Objects.equals(record, recordInfo.record); + } } diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsResponseErrorCode.java b/service-t/src/com/android/server/connectivity/mdns/MdnsResponseErrorCode.java index 73a7e3a5c0..f509da226f 100644 --- a/service-t/src/com/android/server/connectivity/mdns/MdnsResponseErrorCode.java +++ b/service-t/src/com/android/server/connectivity/mdns/MdnsResponseErrorCode.java @@ -37,4 +37,5 @@ public class MdnsResponseErrorCode { public static final int ERROR_END_OF_FILE = 12; public static final int ERROR_READING_NSEC_RDATA = 13; public static final int ERROR_READING_ANY_RDATA = 14; + public static final int ERROR_READING_KEY_RDATA = 15; } \ No newline at end of file diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt index 6c6f6a338b..28685cc387 100644 --- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt +++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt @@ -1463,10 +1463,8 @@ class NsdManagerTest { handlerThread.waitForIdle(TIMEOUT_MS) tryTest { - repeat(3) { - assertNotNull(packetReader.pollForAdvertisement(serviceName, serviceType), - "Expect 3 announcements sent after initial probing") - } + assertNotNull(packetReader.pollForAdvertisement(serviceName, serviceType), + "No announcements sent after initial probing") assertEquals(si.serviceName, registeredService.serviceName) assertEquals(si.hostname, registeredService.hostname) diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt index f7e0b0e88f..d735dc62fe 100644 --- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt +++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt @@ -21,11 +21,13 @@ import android.net.LinkAddress import android.net.nsd.NsdServiceInfo import android.os.Build import android.os.HandlerThread +import com.android.net.module.util.HexDump.hexStringToByteArray import com.android.server.connectivity.mdns.MdnsAnnouncer.AnnouncementInfo import com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_HOST import com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_SERVICE import com.android.server.connectivity.mdns.MdnsRecord.TYPE_A import com.android.server.connectivity.mdns.MdnsRecord.TYPE_AAAA +import com.android.server.connectivity.mdns.MdnsRecord.TYPE_KEY import com.android.server.connectivity.mdns.MdnsRecord.TYPE_PTR import com.android.server.connectivity.mdns.MdnsRecord.TYPE_SRV import com.android.server.connectivity.mdns.MdnsRecord.TYPE_TXT @@ -120,6 +122,20 @@ private val TEST_SERVICE_CUSTOM_HOST_NO_ADDRESSES = NsdServiceInfo().apply { port = TEST_PORT } +private val TEST_PUBLIC_KEY = hexStringToByteArray( + "0201030dc141d0637960b98cbc12cfca" + + "221d2879dac26ee5b460e9007c992e19" + + "02d897c391b03764d448f7d0c772fdb0" + + "3b1d9d6d52ff8886769e8e2362513565" + + "270962d3") + +private val TEST_PUBLIC_KEY_2 = hexStringToByteArray( + "0201030dc141d0637960b98cbc12cfca" + + "221d2879dac26ee5b460e9007c992e19" + + "02d897c391b03764d448f7d0c772fdb0" + + "3b1d9d6d52ff8886769e8e2362513565" + + "270962d4") + @RunWith(DevSdkIgnoreRunner::class) @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2) class MdnsRecordRepositoryTest { @@ -581,6 +597,7 @@ class MdnsRecordRepositoryTest { TYPE_PTR -> return MdnsPointerRecord(name, false /* isUnicast */) TYPE_SRV -> return MdnsServiceRecord(name, false /* isUnicast */) TYPE_TXT -> return MdnsTextRecord(name, false /* isUnicast */) + TYPE_KEY -> return MdnsKeyRecord(name, false /* isUnicast */) TYPE_A, TYPE_AAAA -> return MdnsInetAddressRecord(name, type, false /* isUnicast */) else -> fail("Unexpected question type: $type") } @@ -907,6 +924,159 @@ class MdnsRecordRepositoryTest { reply.additionalAnswers) } + @Test + fun testGetReply_keyQuestionForServiceName_returnsKeyRecord() { + val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags()) + + repository.addServiceAndFinishProbing(TEST_SERVICE_ID_1, NsdServiceInfo().apply { + serviceType = "_testservice._tcp" + serviceName = "MyTestService1" + port = TEST_PORT + publicKey = TEST_PUBLIC_KEY + }) + repository.addServiceAndFinishProbing(TEST_SERVICE_ID_2, NsdServiceInfo().apply { + serviceType = "_testservice._tcp" + serviceName = "MyTestService2" + port = 0 // No SRV RR + publicKey = TEST_PUBLIC_KEY + }) + val src = InetSocketAddress(parseNumericAddress("fe80::123"), 5353) + val serviceName1 = arrayOf("MyTestService1", "_testservice", "_tcp", "local") + val serviceName2 = arrayOf("MyTestService2", "_testservice", "_tcp", "local") + + val query1 = makeQuery(TYPE_KEY to serviceName1) + val reply1 = repository.getReply(query1, src) + + assertNotNull(reply1) + assertEquals(listOf(MdnsKeyRecord(serviceName1, + 0, false, LONG_TTL, TEST_PUBLIC_KEY)), + reply1.answers) + assertEquals(listOf(), + reply1.additionalAnswers) + + val query2 = makeQuery(TYPE_KEY to serviceName2) + val reply2 = repository.getReply(query2, src) + + assertNotNull(reply2) + assertEquals(listOf(MdnsKeyRecord(serviceName2, + 0, false, LONG_TTL, TEST_PUBLIC_KEY)), + reply2.answers) + assertEquals(listOf(MdnsNsecRecord(serviceName2, + 0L, true, SHORT_TTL, + serviceName2 /* nextDomain */, + intArrayOf(TYPE_KEY))), + reply2.additionalAnswers) + } + + @Test + fun testGetReply_keyQuestionForHostname_returnsKeyRecord() { + val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags()) + + repository.addServiceAndFinishProbing(TEST_SERVICE_ID_1, NsdServiceInfo().apply { + hostname = "MyHost1" + hostAddresses = listOf( + parseNumericAddress("2001:db8::1"), + parseNumericAddress("2001:db8::2")) + publicKey = TEST_PUBLIC_KEY + }) + repository.addServiceAndFinishProbing(TEST_SERVICE_ID_2, NsdServiceInfo().apply { + hostname = "MyHost2" + hostAddresses = listOf() // No address records + publicKey = TEST_PUBLIC_KEY + }) + val src = InetSocketAddress(parseNumericAddress("fe80::123"), 5353) + val hostname1 = arrayOf("MyHost1", "local") + val hostname2 = arrayOf("MyHost2", "local") + + val query1 = makeQuery(TYPE_KEY to hostname1) + val reply1 = repository.getReply(query1, src) + + assertNotNull(reply1) + assertEquals(listOf(MdnsKeyRecord(hostname1, + 0, false, LONG_TTL, TEST_PUBLIC_KEY)), + reply1.answers) + assertEquals(listOf(), + reply1.additionalAnswers) + + val query2 = makeQuery(TYPE_KEY to hostname2) + val reply2 = repository.getReply(query2, src) + + assertNotNull(reply2) + assertEquals(listOf(MdnsKeyRecord(hostname2, + 0, false, LONG_TTL, TEST_PUBLIC_KEY)), + reply2.answers) + assertEquals(listOf(MdnsNsecRecord(hostname2, 0L, true, SHORT_TTL, + hostname2 /* nextDomain */, + intArrayOf(TYPE_KEY))), + reply2.additionalAnswers) + } + + @Test + fun testGetReply_keyRecordForHostRemoved_noAnswertoKeyQuestion() { + val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags()) + + repository.addServiceAndFinishProbing(TEST_SERVICE_ID_1, NsdServiceInfo().apply { + hostname = "MyHost1" + hostAddresses = listOf( + parseNumericAddress("2001:db8::1"), + parseNumericAddress("2001:db8::2")) + publicKey = TEST_PUBLIC_KEY + }) + repository.addServiceAndFinishProbing(TEST_SERVICE_ID_2, NsdServiceInfo().apply { + hostname = "MyHost2" + hostAddresses = listOf() // No address records + publicKey = TEST_PUBLIC_KEY + }) + repository.removeService(TEST_SERVICE_ID_1) + repository.removeService(TEST_SERVICE_ID_2) + val src = InetSocketAddress(parseNumericAddress("fe80::123"), 5353) + val hostname1 = arrayOf("MyHost1", "local") + val hostname2 = arrayOf("MyHost2", "local") + + val query1 = makeQuery(TYPE_KEY to hostname1) + val reply1 = repository.getReply(query1, src) + + assertNull(reply1) + + val query2 = makeQuery(TYPE_KEY to hostname2) + val reply2 = repository.getReply(query2, src) + + assertNull(reply2) + } + + @Test + fun testGetReply_keyRecordForServiceRemoved_noAnswertoKeyQuestion() { + val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags()) + + repository.addServiceAndFinishProbing(TEST_SERVICE_ID_1, NsdServiceInfo().apply { + serviceType = "_testservice._tcp" + serviceName = "MyTestService1" + port = TEST_PORT + publicKey = TEST_PUBLIC_KEY + }) + repository.addServiceAndFinishProbing(TEST_SERVICE_ID_2, NsdServiceInfo().apply { + serviceType = "_testservice._tcp" + serviceName = "MyTestService2" + port = 0 // No SRV RR + publicKey = TEST_PUBLIC_KEY + }) + repository.removeService(TEST_SERVICE_ID_1) + repository.removeService(TEST_SERVICE_ID_2) + val src = InetSocketAddress(parseNumericAddress("fe80::123"), 5353) + val serviceName1 = arrayOf("MyTestService1", "_testservice", "_tcp", "local") + val serviceName2 = arrayOf("MyTestService2", "_testservice", "_tcp", "local") + + val query1 = makeQuery(TYPE_KEY to serviceName1) + val reply1 = repository.getReply(query1, src) + + assertNull(reply1) + + val query2 = makeQuery(TYPE_KEY to serviceName2) + val reply2 = repository.getReply(query2, src) + + assertNull(reply2) + } + @Test fun testGetReply_customHostRemoved_noAnswerToAAAAQuestion() { val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags()) @@ -1221,8 +1391,8 @@ class MdnsRecordRepositoryTest { @Test fun testGetConflictingServices_customHostsReplyHasFewerAddressesThanUs_noConflict() { val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags()) - repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, null /* ttl */) - repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */) + repository.addServiceAndFinishProbing(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1) + repository.addServiceAndFinishProbing(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2) val packet = MdnsPacket( 0, /* flags */ @@ -1240,10 +1410,30 @@ class MdnsRecordRepositoryTest { } @Test - fun testGetConflictingServices_customHostsReplyHasIdenticalHosts_noConflict() { + fun testGetConflictingServices_customHostsReplyHasSameNameRecord_conflictDuringProbing() { val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags()) repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, null /* ttl */) - repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */) + repository.addServiceAndFinishProbing(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2) + + val packet = MdnsPacket( + 0, /* flags */ + emptyList(), /* questions */ + listOf(MdnsKeyRecord(arrayOf("TestHost", "local"), + 0L /* receiptTimeMillis */, true /* cacheFlush */, + 0L /* ttlMillis */, TEST_PUBLIC_KEY), + ) /* answers */, + emptyList() /* authorityRecords */, + emptyList() /* additionalRecords */) + + assertEquals(mapOf(TEST_CUSTOM_HOST_ID_1 to CONFLICT_HOST), + repository.getConflictingServices(packet)) + } + + @Test + fun testGetConflictingServices_customHostsReplyHasIdenticalHosts_noConflict() { + val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags()) + repository.addServiceAndFinishProbing(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1) + repository.addServiceAndFinishProbing(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2) val packet = MdnsPacket( 0, /* flags */ @@ -1267,8 +1457,8 @@ class MdnsRecordRepositoryTest { @Test fun testGetConflictingServices_customHostsCaseInsensitiveReplyHasIdenticalHosts_noConflict() { val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags()) - repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, null /* ttl */) - repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */) + repository.addServiceAndFinishProbing(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1) + repository.addServiceAndFinishProbing(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2) val packet = MdnsPacket( 0, /* flags */ @@ -1288,6 +1478,152 @@ class MdnsRecordRepositoryTest { repository.getConflictingServices(packet)) } + @Test + fun testGetConflictingServices_identicalKeyRecordsForService_noConflict() { + val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags()) + + repository.addService(TEST_SERVICE_ID_1, NsdServiceInfo().apply { + serviceType = "_testservice._tcp" + serviceName = "MyTestService" + port = TEST_PORT + publicKey = TEST_PUBLIC_KEY + }, null /* ttl */) + + val otherTtlMillis = 1234L + val packet = MdnsPacket( + 0 /* flags */, + emptyList() /* questions */, + listOf( + MdnsKeyRecord( + arrayOf("MyTestService", "_testservice", "_tcp", "local"), + 0L /* receiptTimeMillis */, true /* cacheFlush */, + otherTtlMillis, + TEST_PUBLIC_KEY) + ) /* answers */, + emptyList() /* authorityRecords */, + emptyList() /* additionalRecords */) + + assertEquals(emptyMap(), + repository.getConflictingServices(packet)) + } + + @Test + fun testGetConflictingServices_differentKeyRecordsForService_conflict() { + val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags()) + + repository.addService(TEST_SERVICE_ID_1, NsdServiceInfo().apply { + serviceType = "_testservice._tcp" + serviceName = "MyTestService" + port = TEST_PORT + publicKey = TEST_PUBLIC_KEY + }, null /* null */) + + val otherTtlMillis = 1234L + val packet = MdnsPacket( + 0 /* flags */, + emptyList() /* questions */, + listOf( + MdnsKeyRecord( + arrayOf("MyTestService", "_testservice", "_tcp", "local"), + 0L /* receiptTimeMillis */, true /* cacheFlush */, + otherTtlMillis, + TEST_PUBLIC_KEY_2) + ) /* answers */, + emptyList() /* authorityRecords */, + emptyList() /* additionalRecords */) + + assertEquals(mapOf(TEST_SERVICE_ID_1 to CONFLICT_SERVICE), + repository.getConflictingServices(packet)) + } + + @Test + fun testGetConflictingServices_identicalKeyRecordsForHost_noConflict() { + val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags()) + + repository.addServiceAndFinishProbing(TEST_SERVICE_ID_1, NsdServiceInfo().apply { + hostname = "MyHost" + hostAddresses = listOf( + parseNumericAddress("2001:db8::1"), + parseNumericAddress("2001:db8::2") + ) + publicKey = TEST_PUBLIC_KEY + }) + + val otherTtlMillis = 1234L + val packet = MdnsPacket( + 0 /* flags */, + emptyList() /* questions */, + listOf( + MdnsKeyRecord( + arrayOf("MyHost", "local"), + 0L /* receiptTimeMillis */, true /* cacheFlush */, + otherTtlMillis, + TEST_PUBLIC_KEY) + ) /* answers */, + emptyList() /* authorityRecords */, + emptyList() /* additionalRecords */) + + assertEquals(emptyMap(), + repository.getConflictingServices(packet)) + } + + @Test + fun testGetConflictingServices_keyForCustomHostReplySameRecordName_conflictDuringProbing() { + val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags()) + + repository.addService(TEST_SERVICE_ID_1, NsdServiceInfo().apply { + hostname = "MyHost" + publicKey = TEST_PUBLIC_KEY + }, null /* ttl */) + + val otherTtlMillis = 1234L + val packet = MdnsPacket( + 0 /* flags */, + emptyList() /* questions */, + listOf(MdnsInetAddressRecord(arrayOf("MyHost", "local"), + 0L /* receiptTimeMillis */, + true /* cacheFlush */, + otherTtlMillis, + parseNumericAddress("192.168.2.111")) + ) /* answers */, + emptyList() /* authorityRecords */, + emptyList() /* additionalRecords */ + ) + + assertEquals(mapOf(TEST_SERVICE_ID_1 to CONFLICT_HOST), + repository.getConflictingServices(packet)) + } + + @Test + fun testGetConflictingServices_differentKeyRecordsForHost_conflict() { + val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags()) + + repository.addService(TEST_SERVICE_ID_1, NsdServiceInfo().apply { + hostname = "MyHost" + hostAddresses = listOf( + parseNumericAddress("2001:db8::1"), + parseNumericAddress("2001:db8::2")) + publicKey = TEST_PUBLIC_KEY + }, null /* ttl */) + + val otherTtlMillis = 1234L + val packet = MdnsPacket( + 0 /* flags */, + emptyList() /* questions */, + listOf( + MdnsKeyRecord( + arrayOf("MyHost", "local"), + 0L /* receiptTimeMillis */, true /* cacheFlush */, + otherTtlMillis, + TEST_PUBLIC_KEY_2) + ) /* answers */, + emptyList() /* authorityRecords */, + emptyList() /* additionalRecords */) + + assertEquals(mapOf(TEST_SERVICE_ID_1 to CONFLICT_HOST), + repository.getConflictingServices(packet)) + } + @Test fun testGetConflictingServices_IdenticalService() { val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags()) diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java index 55c2846c73..63548c10e9 100644 --- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java +++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java @@ -16,11 +16,13 @@ package com.android.server.connectivity.mdns; +import static com.android.server.connectivity.mdns.MdnsConstants.QCLASS_INTERNET; import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; @@ -424,4 +426,92 @@ public class MdnsRecordTests { assertEquals(new TextEntry("xyz", HexDump.hexStringToByteArray("FFEFDFCF")), entries.get(2)); } + + @Test + public void testKeyRecord() throws IOException { + final byte[] dataIn = + HexDump.hexStringToByteArray( + "09746573742d686f7374056c6f63616c" + + "00001980010000000a00440201030dc1" + + "41d0637960b98cbc12cfca221d2879da" + + "c26ee5b460e9007c992e1902d897c391" + + "b03764d448f7d0c772fdb03b1d9d6d52" + + "ff8886769e8e2362513565270962d3"); + final byte[] rData = + HexDump.hexStringToByteArray( + "0201030dc141d0637960b98cbc12cfca" + + "221d2879dac26ee5b460e9007c992e19" + + "02d897c391b03764d448f7d0c772fdb0" + + "3b1d9d6d52ff8886769e8e2362513565" + + "270962d3"); + assertNotNull(dataIn); + String dataInText = HexDump.dumpHexString(dataIn, 0, dataIn.length); + + // Decode + DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length); + MdnsPacketReader reader = new MdnsPacketReader(packet); + + String[] name = reader.readLabels(); + assertNotNull(name); + assertEquals(2, name.length); + String fqdn = MdnsRecord.labelsToString(name); + assertEquals("test-host.local", fqdn); + + int type = reader.readUInt16(); + assertEquals(MdnsRecord.TYPE_KEY, type); + + MdnsKeyRecord keyRecord; + + // MdnsKeyRecord(String[] name, MdnsPacketReader reader) + reader = new MdnsPacketReader(packet); + reader.readLabels(); // Skip labels + reader.readUInt16(); // Skip type + keyRecord = new MdnsKeyRecord(name, reader); + assertEquals(MdnsRecord.TYPE_KEY, keyRecord.getType()); + assertTrue(keyRecord.getTtl() > 0); // Not a question so the TTL is greater than 0 + assertTrue(keyRecord.getCacheFlush()); + assertArrayEquals(new String[] {"test-host", "local"}, keyRecord.getName()); + assertArrayEquals(rData, keyRecord.getRData()); + assertNotEquals(rData, keyRecord.getRData()); // Uses a copy of the original RDATA + assertEquals(dataInText, toHex(keyRecord)); + + // MdnsKeyRecord(String[] name, MdnsPacketReader reader, boolean isQuestion) + reader = new MdnsPacketReader(packet); + reader.readLabels(); // Skip labels + reader.readUInt16(); // Skip type + keyRecord = new MdnsKeyRecord(name, reader, false /* isQuestion */); + assertEquals(MdnsRecord.TYPE_KEY, keyRecord.getType()); + assertTrue(keyRecord.getTtl() > 0); // Not a question, so the TTL is greater than 0 + assertTrue(keyRecord.getCacheFlush()); + assertArrayEquals(new String[] {"test-host", "local"}, keyRecord.getName()); + assertArrayEquals(rData, keyRecord.getRData()); + assertNotEquals(rData, keyRecord.getRData()); // Uses a copy of the original RDATA + + // MdnsKeyRecord(String[] name, boolean isUnicast) + keyRecord = new MdnsKeyRecord(name, false /* isUnicast */); + assertEquals(MdnsRecord.TYPE_KEY, keyRecord.getType()); + assertEquals(0, keyRecord.getTtl()); + assertEquals(QCLASS_INTERNET, keyRecord.getRecordClass()); + assertFalse(keyRecord.getCacheFlush()); + assertArrayEquals(new String[] {"test-host", "local"}, keyRecord.getName()); + assertArrayEquals(null, keyRecord.getRData()); + + // MdnsKeyRecord(String[] name, long receiptTimeMillis, boolean cacheFlush, long ttlMillis, + // byte[] rData) + keyRecord = + new MdnsKeyRecord( + name, + 10 /* receiptTimeMillis */, + true /* cacheFlush */, + 20_000 /* ttlMillis */, + rData); + assertEquals(MdnsRecord.TYPE_KEY, keyRecord.getType()); + assertEquals(10, keyRecord.getReceiptTime()); + assertTrue(keyRecord.getCacheFlush()); + assertEquals(20_000, keyRecord.getTtl()); + assertEquals(QCLASS_INTERNET, keyRecord.getRecordClass()); + assertArrayEquals(new String[] {"test-host", "local"}, keyRecord.getName()); + assertArrayEquals(rData, keyRecord.getRData()); + assertNotEquals(rData, keyRecord.getRData()); // Uses a copy of the original RDATA + } }