1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.connectivity.mdns;
18 
19 import static com.android.server.connectivity.mdns.MdnsConstants.IPV4_SOCKET_ADDR;
20 import static com.android.server.connectivity.mdns.MdnsConstants.IPV6_SOCKET_ADDR;
21 import static com.android.server.connectivity.mdns.MdnsConstants.NO_PACKET;
22 import static com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_HOST;
23 import static com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_SERVICE;
24 
25 import android.annotation.NonNull;
26 import android.annotation.Nullable;
27 import android.annotation.TargetApi;
28 import android.net.LinkAddress;
29 import android.net.nsd.NsdServiceInfo;
30 import android.os.Build;
31 import android.os.Looper;
32 import android.os.SystemClock;
33 import android.text.TextUtils;
34 import android.util.ArrayMap;
35 import android.util.ArraySet;
36 import android.util.SparseArray;
37 import android.util.SparseIntArray;
38 
39 import com.android.internal.annotations.VisibleForTesting;
40 import com.android.net.module.util.CollectionUtils;
41 import com.android.net.module.util.HexDump;
42 import com.android.server.connectivity.mdns.util.MdnsUtils;
43 
44 import java.io.IOException;
45 import java.net.Inet4Address;
46 import java.net.InetAddress;
47 import java.net.InetSocketAddress;
48 import java.net.NetworkInterface;
49 import java.time.Duration;
50 import java.util.ArrayList;
51 import java.util.Arrays;
52 import java.util.Collections;
53 import java.util.Enumeration;
54 import java.util.Iterator;
55 import java.util.LinkedHashSet;
56 import java.util.List;
57 import java.util.Map;
58 import java.util.Objects;
59 import java.util.Random;
60 import java.util.Set;
61 import java.util.TreeMap;
62 import java.util.concurrent.TimeUnit;
63 import java.util.function.BiConsumer;
64 import java.util.function.Consumer;
65 
66 /**
67  * A repository of records advertised through {@link MdnsInterfaceAdvertiser}.
68  *
69  * Must be used on a consistent looper thread.
70  */
71 @TargetApi(Build.VERSION_CODES.TIRAMISU) // Allow calling T+ APIs; this is only loaded on T+
72 public class MdnsRecordRepository {
73     // RFC6762 p.15
74     private static final long MIN_MULTICAST_REPLY_INTERVAL_MS = 1_000L;
75 
76     // TTLs as per RFC6762 10.
77     // TTL for records with a host name as the resource record's name (e.g., A, AAAA, HINFO) or a
78     // host name contained within the resource record's rdata (e.g., SRV, reverse mapping PTR
79     // record)
80     private static final long DEFAULT_NAME_RECORDS_TTL_MILLIS = TimeUnit.SECONDS.toMillis(120);
81     // TTL for other records
82     private static final long DEFAULT_NON_NAME_RECORDS_TTL_MILLIS = TimeUnit.MINUTES.toMillis(75);
83 
84     // Top-level domain for link-local queries, as per RFC6762 3.
85     private static final String LOCAL_TLD = "local";
86 
87     // Service type for service enumeration (RFC6763 9.)
88     private static final String[] DNS_SD_SERVICE_TYPE =
89             new String[] { "_services", "_dns-sd", "_udp", LOCAL_TLD };
90 
91     private enum RecordConflictType {
92         NO_CONFLICT,
93         CONFLICT,
94         IDENTICAL
95     }
96 
97     @NonNull
98     private final Random mDelayGenerator = new Random();
99     // Map of service unique ID -> records for service
100     @NonNull
101     private final SparseArray<ServiceRegistration> mServices = new SparseArray<>();
102     @NonNull
103     private final List<RecordInfo<?>> mGeneralRecords = new ArrayList<>();
104     @NonNull
105     private final Looper mLooper;
106     @NonNull
107     private final Dependencies mDeps;
108     @NonNull
109     private final String[] mDeviceHostname;
110     @NonNull
111     private final MdnsFeatureFlags mMdnsFeatureFlags;
112 
MdnsRecordRepository(@onNull Looper looper, @NonNull String[] deviceHostname, @NonNull MdnsFeatureFlags mdnsFeatureFlags)113     public MdnsRecordRepository(@NonNull Looper looper, @NonNull String[] deviceHostname,
114             @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
115         this(looper, new Dependencies(), deviceHostname, mdnsFeatureFlags);
116     }
117 
118     @VisibleForTesting
MdnsRecordRepository(@onNull Looper looper, @NonNull Dependencies deps, @NonNull String[] deviceHostname, @NonNull MdnsFeatureFlags mdnsFeatureFlags)119     public MdnsRecordRepository(@NonNull Looper looper, @NonNull Dependencies deps,
120             @NonNull String[] deviceHostname, @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
121         mDeviceHostname = deviceHostname;
122         mLooper = looper;
123         mDeps = deps;
124         mMdnsFeatureFlags = mdnsFeatureFlags;
125     }
126 
127     /**
128      * Dependencies to use with {@link MdnsRecordRepository}, useful for testing.
129      */
130     @VisibleForTesting
131     public static class Dependencies {
132 
133         /**
134          * @see NetworkInterface#getInetAddresses().
135          */
136         @NonNull
getInterfaceInetAddresses(@onNull NetworkInterface iface)137         public Enumeration<InetAddress> getInterfaceInetAddresses(@NonNull NetworkInterface iface) {
138             return iface.getInetAddresses();
139         }
140 
elapsedRealTime()141         public long elapsedRealTime() {
142             return SystemClock.elapsedRealtime();
143         }
144     }
145 
146     private static class RecordInfo<T extends MdnsRecord> {
147         public final T record;
148         public final NsdServiceInfo serviceInfo;
149 
150         /**
151          * Whether the name of this record is expected to be fully owned by the service or may be
152          * advertised by other hosts as well (shared).
153          */
154         public final boolean isSharedName;
155 
156         /**
157          * Last time (as per SystemClock.elapsedRealtime) when advertised via multicast on IPv4, 0
158          * if never
159          */
160         public long lastAdvertisedOnIpv4TimeMs;
161 
162         /**
163          * Last time (as per SystemClock.elapsedRealtime) when advertised via multicast on IPv6, 0
164          * if never
165          */
166         public long lastAdvertisedOnIpv6TimeMs;
167 
168         /**
169          * Last time (as per SystemClock.elapsedRealtime) when sent via unicast or multicast, 0 if
170          * never.
171          *
172          * <p>Different from lastAdvertisedOnIpv(4|6)TimeMs, lastSentTimeMs is mainly used for
173          * tracking is a record is ever sent out, no matter unicast/multicast or IPv4/IPv6. It's
174          * unnecessary to maintain two versions (IPv4/IPv6) for it.
175          */
176         public long lastSentTimeMs;
177 
RecordInfo(NsdServiceInfo serviceInfo, T record, boolean sharedName)178         RecordInfo(NsdServiceInfo serviceInfo, T record, boolean sharedName) {
179             this.serviceInfo = serviceInfo;
180             this.record = record;
181             this.isSharedName = sharedName;
182         }
183     }
184 
185     private static class ServiceRegistration {
186         @NonNull
187         public final List<RecordInfo<?>> allRecords;
188         @NonNull
189         public final List<RecordInfo<MdnsPointerRecord>> ptrRecords;
190         @Nullable
191         public final RecordInfo<MdnsServiceRecord> srvRecord;
192         @Nullable
193         public final RecordInfo<MdnsTextRecord> txtRecord;
194         @Nullable
195         public final RecordInfo<MdnsKeyRecord> serviceKeyRecord;
196         @Nullable
197         public final RecordInfo<MdnsKeyRecord> hostKeyRecord;
198         @NonNull
199         public final List<RecordInfo<MdnsInetAddressRecord>> addressRecords;
200         @NonNull
201         public final NsdServiceInfo serviceInfo;
202 
203         /**
204          * Whether the service is sending exit announcements and will be destroyed soon.
205          */
206         public boolean exiting;
207 
208         /**
209          * The replied query packet count of this service.
210          */
211         public int repliedServiceCount = NO_PACKET;
212 
213         /**
214          * The sent packet count of this service (including announcements and probes).
215          */
216         public int sentPacketCount = NO_PACKET;
217 
218         /**
219          * Whether probing is still in progress.
220          */
221         private boolean isProbing;
222 
223         @Nullable
224         private Duration ttl;
225 
226         /**
227          * Create a ServiceRegistration with only update the subType.
228          */
withSubtypes(@onNull Set<String> newSubtypes)229         ServiceRegistration withSubtypes(@NonNull Set<String> newSubtypes) {
230             NsdServiceInfo newServiceInfo = new NsdServiceInfo(serviceInfo);
231             newServiceInfo.setSubtypes(newSubtypes);
232             return new ServiceRegistration(srvRecord.record.getServiceHost(), newServiceInfo,
233                     repliedServiceCount, sentPacketCount, exiting, isProbing, ttl);
234         }
235 
236         /**
237          * Create a ServiceRegistration for dns-sd service registration (RFC6763).
238          */
ServiceRegistration(@onNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo, int repliedServiceCount, int sentPacketCount, boolean exiting, boolean isProbing, @Nullable Duration ttl)239         ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
240                 int repliedServiceCount, int sentPacketCount, boolean exiting, boolean isProbing,
241                 @Nullable Duration ttl) {
242             this.serviceInfo = serviceInfo;
243 
244             final long nonNameRecordsTtlMillis;
245             final long nameRecordsTtlMillis;
246 
247             // When custom TTL is specified, all records of the service will use the custom TTL.
248             // This is typically useful for SRP (Service Registration Protocol:
249             // https://datatracker.ietf.org/doc/html/draft-ietf-dnssd-srp-24) Advertising Proxy
250             // where all records in a single SRP are required the same TTL.
251             if (ttl != null) {
252                 nonNameRecordsTtlMillis = ttl.toMillis();
253                 nameRecordsTtlMillis = ttl.toMillis();
254             } else {
255                 nonNameRecordsTtlMillis = DEFAULT_NON_NAME_RECORDS_TTL_MILLIS;
256                 nameRecordsTtlMillis = DEFAULT_NAME_RECORDS_TTL_MILLIS;
257             }
258 
259             final boolean hasCustomHost = !TextUtils.isEmpty(serviceInfo.getHostname());
260             final String[] hostname =
261                     hasCustomHost
262                             ? new String[] {serviceInfo.getHostname(), LOCAL_TLD}
263                             : deviceHostname;
264             final ArrayList<RecordInfo<?>> allRecords = new ArrayList<>(5);
265 
266             final boolean hasService = !TextUtils.isEmpty(serviceInfo.getServiceType());
267             final String[] serviceType = hasService ? splitServiceType(serviceInfo) : null;
268             final String[] serviceName =
269                     hasService ? splitFullyQualifiedName(serviceInfo, serviceType) : null;
270             if (hasService && hasSrvRecord(serviceInfo)) {
271                 // Service PTR records
272                 ptrRecords = new ArrayList<>(serviceInfo.getSubtypes().size() + 1);
273                 ptrRecords.add(new RecordInfo<>(
274                         serviceInfo,
275                         new MdnsPointerRecord(
276                                 serviceType,
277                                 0L /* receiptTimeMillis */,
278                                 false /* cacheFlush */,
279                                 nonNameRecordsTtlMillis,
280                                 serviceName),
281                         true /* sharedName */));
282                 for (String subtype : serviceInfo.getSubtypes()) {
283                     ptrRecords.add(new RecordInfo<>(
284                             serviceInfo,
285                             new MdnsPointerRecord(
286                                     MdnsUtils.constructFullSubtype(serviceType, subtype),
287                                     0L /* receiptTimeMillis */,
288                                     false /* cacheFlush */,
289                                     nonNameRecordsTtlMillis,
290                                     serviceName),
291                             true /* sharedName */));
292                 }
293 
294                 srvRecord = new RecordInfo<>(
295                         serviceInfo,
296                         new MdnsServiceRecord(serviceName,
297                                 0L /* receiptTimeMillis */,
298                                 true /* cacheFlush */,
299                                 nameRecordsTtlMillis,
300                                 0 /* servicePriority */, 0 /* serviceWeight */,
301                                 serviceInfo.getPort(),
302                                 hostname),
303                         false /* sharedName */);
304 
305                 txtRecord = new RecordInfo<>(
306                         serviceInfo,
307                         new MdnsTextRecord(serviceName,
308                                 0L /* receiptTimeMillis */,
309                                 // Service name is verified unique after probing
310                                 true /* cacheFlush */,
311                                 nonNameRecordsTtlMillis,
312                                 attrsToTextEntries(serviceInfo.getAttributes())),
313                         false /* sharedName */);
314 
315                 allRecords.addAll(ptrRecords);
316                 allRecords.add(srvRecord);
317                 allRecords.add(txtRecord);
318                 // Service type enumeration record (RFC6763 9.)
319                 allRecords.add(new RecordInfo<>(
320                         serviceInfo,
321                         new MdnsPointerRecord(
322                                 DNS_SD_SERVICE_TYPE,
323                                 0L /* receiptTimeMillis */,
324                                 false /* cacheFlush */,
325                                 nonNameRecordsTtlMillis,
326                                 serviceType),
327                         true /* sharedName */));
328             } else {
329                 ptrRecords = Collections.emptyList();
330                 srvRecord = null;
331                 txtRecord = null;
332             }
333 
334             if (hasCustomHost) {
335                 addressRecords = new ArrayList<>(serviceInfo.getHostAddresses().size());
336                 for (InetAddress address : serviceInfo.getHostAddresses()) {
337                     addressRecords.add(new RecordInfo<>(
338                                     serviceInfo,
339                                     new MdnsInetAddressRecord(hostname,
340                                             0L /* receiptTimeMillis */,
341                                             true /* cacheFlush */,
342                                             nameRecordsTtlMillis,
343                                             address),
344                                     false /* sharedName */));
345                 }
346                 allRecords.addAll(addressRecords);
347             } else {
348                 addressRecords = Collections.emptyList();
349             }
350 
351             final boolean hasKey = hasKeyRecord(serviceInfo);
352             if (hasKey && hasService) {
353                 this.serviceKeyRecord = new RecordInfo<>(
354                         serviceInfo,
355                         new MdnsKeyRecord(
356                                 serviceName,
357                                 0L /*receiptTimeMillis */,
358                                 true /* cacheFlush */,
359                                 nameRecordsTtlMillis,
360                                 serviceInfo.getPublicKey()),
361                         false /* sharedName */);
362                 allRecords.add(this.serviceKeyRecord);
363             } else {
364                 this.serviceKeyRecord = null;
365             }
366             if (hasKey && hasCustomHost) {
367                 this.hostKeyRecord = new RecordInfo<>(
368                         serviceInfo,
369                         new MdnsKeyRecord(
370                                 hostname,
371                                 0L /*receiptTimeMillis */,
372                                 true /* cacheFlush */,
373                                 nameRecordsTtlMillis,
374                                 serviceInfo.getPublicKey()),
375                         false /* sharedName */);
376                 allRecords.add(this.hostKeyRecord);
377             } else {
378                 this.hostKeyRecord = null;
379             }
380 
381             this.allRecords = Collections.unmodifiableList(allRecords);
382             this.repliedServiceCount = repliedServiceCount;
383             this.sentPacketCount = sentPacketCount;
384             this.isProbing = isProbing;
385             this.exiting = exiting;
386         }
387 
388         /**
389          * Create a ServiceRegistration for dns-sd service registration (RFC6763).
390          *
391          * @param deviceHostname Hostname of the device (for the interface used)
392          * @param serviceInfo Service to advertise
393          */
ServiceRegistration(@onNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo, int repliedServiceCount, int sentPacketCount, @Nullable Duration ttl)394         ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
395                 int repliedServiceCount, int sentPacketCount, @Nullable Duration ttl) {
396             this(deviceHostname, serviceInfo,repliedServiceCount, sentPacketCount,
397                     false /* exiting */, true /* isProbing */, ttl);
398         }
399 
setProbing(boolean probing)400         void setProbing(boolean probing) {
401             this.isProbing = probing;
402         }
403 
404     }
405 
406     /**
407      * Inform the repository of the latest interface addresses.
408      */
updateAddresses(@onNull List<LinkAddress> newAddresses)409     public void updateAddresses(@NonNull List<LinkAddress> newAddresses) {
410         mGeneralRecords.clear();
411         for (LinkAddress addr : newAddresses) {
412             final String[] revDnsAddr = getReverseDnsAddress(addr.getAddress());
413             mGeneralRecords.add(new RecordInfo<>(
414                     null /* serviceInfo */,
415                     new MdnsPointerRecord(
416                             revDnsAddr,
417                             0L /* receiptTimeMillis */,
418                             true /* cacheFlush */,
419                             DEFAULT_NAME_RECORDS_TTL_MILLIS,
420                             mDeviceHostname),
421                     false /* sharedName */));
422 
423             mGeneralRecords.add(new RecordInfo<>(
424                     null /* serviceInfo */,
425                     new MdnsInetAddressRecord(
426                             mDeviceHostname,
427                             0L /* receiptTimeMillis */,
428                             true /* cacheFlush */,
429                             DEFAULT_NAME_RECORDS_TTL_MILLIS,
430                             addr.getAddress()),
431                     false /* sharedName */));
432         }
433     }
434 
435     /**
436      * Update a service that already registered in the repository.
437      *
438      * @param serviceId An existing service ID.
439      * @param subtypes New subtypes
440      */
updateService(int serviceId, @NonNull Set<String> subtypes)441     public void updateService(int serviceId, @NonNull Set<String> subtypes) {
442         final ServiceRegistration existingRegistration = mServices.get(serviceId);
443         if (existingRegistration == null) {
444             throw new IllegalArgumentException(
445                     "Service ID must already exist for an update request: " + serviceId);
446         }
447         final ServiceRegistration updatedRegistration = existingRegistration.withSubtypes(
448                 subtypes);
449         mServices.put(serviceId, updatedRegistration);
450     }
451 
452     /**
453      * Add a service to the repository.
454      *
455      * This may remove/replace any existing service that used the name added but is exiting.
456      * @param serviceId A unique service ID.
457      * @param serviceInfo Service info to add.
458      * @param ttl the TTL duration for all records of {@code serviceInfo} or {@code null}
459      * @return If the added service replaced another with a matching name (which was exiting), the
460      *         ID of the replaced service.
461      * @throws NameConflictException There is already a (non-exiting) service using the name.
462      */
addService(int serviceId, NsdServiceInfo serviceInfo, @Nullable Duration ttl)463     public int addService(int serviceId, NsdServiceInfo serviceInfo, @Nullable Duration ttl)
464             throws NameConflictException {
465         if (mServices.contains(serviceId)) {
466             throw new IllegalArgumentException(
467                     "Service ID must not be reused across registrations: " + serviceId);
468         }
469 
470         final int existing =
471                 getServiceByNameAndType(serviceInfo.getServiceName(), serviceInfo.getServiceType());
472         // It's OK to re-add a service that is exiting
473         if (existing >= 0 && !mServices.get(existing).exiting) {
474             throw new NameConflictException(existing);
475         }
476 
477         final ServiceRegistration registration = new ServiceRegistration(
478                 mDeviceHostname, serviceInfo, NO_PACKET /* repliedServiceCount */,
479                 NO_PACKET /* sentPacketCount */, ttl);
480         mServices.put(serviceId, registration);
481 
482         // Remove existing exiting service
483         mServices.remove(existing);
484         return existing;
485     }
486 
487     /**
488      * @return The ID of the service identified by its name and type, or -1 if none.
489      */
getServiceByNameAndType( @ullable String serviceName, @Nullable String serviceType)490     private int getServiceByNameAndType(
491             @Nullable String serviceName, @Nullable String serviceType) {
492         if (TextUtils.isEmpty(serviceName) || TextUtils.isEmpty(serviceType)) {
493             return -1;
494         }
495         for (int i = 0; i < mServices.size(); i++) {
496             final NsdServiceInfo info = mServices.valueAt(i).serviceInfo;
497             if (MdnsUtils.equalsIgnoreDnsCase(serviceName, info.getServiceName())
498                     && MdnsUtils.equalsIgnoreDnsCase(serviceType, info.getServiceType())) {
499                 return mServices.keyAt(i);
500             }
501         }
502         return -1;
503     }
504 
makeProbingInfo( int serviceId, ServiceRegistration registration)505     private MdnsProber.ProbingInfo makeProbingInfo(
506             int serviceId, ServiceRegistration registration) {
507         final List<MdnsRecord> probingRecords = new ArrayList<>();
508         // Probe with cacheFlush cleared; it is set when announcing, as it was verified unique:
509         // RFC6762 10.2
510         if (registration.srvRecord != null) {
511             MdnsServiceRecord srvRecord = registration.srvRecord.record;
512             probingRecords.add(new MdnsServiceRecord(srvRecord.getName(),
513                     0L /* receiptTimeMillis */,
514                     false /* cacheFlush */,
515                     srvRecord.getTtl(),
516                     srvRecord.getServicePriority(), srvRecord.getServiceWeight(),
517                     srvRecord.getServicePort(),
518                     srvRecord.getServiceHost()));
519         }
520 
521         for (MdnsInetAddressRecord inetAddressRecord :
522                 makeProbingInetAddressRecords(registration.serviceInfo)) {
523             probingRecords.add(new MdnsInetAddressRecord(inetAddressRecord.getName(),
524                     0L /* receiptTimeMillis */,
525                     false /* cacheFlush */,
526                     inetAddressRecord.getTtl(),
527                     inetAddressRecord.getInet4Address() == null
528                             ? inetAddressRecord.getInet6Address()
529                             : inetAddressRecord.getInet4Address()));
530         }
531 
532         List<MdnsKeyRecord> keyRecords = new ArrayList<>();
533         if (registration.serviceKeyRecord != null) {
534             keyRecords.add(registration.serviceKeyRecord.record);
535         }
536         if (registration.hostKeyRecord != null) {
537             keyRecords.add(registration.hostKeyRecord.record);
538         }
539         for (MdnsKeyRecord keyRecord : keyRecords) {
540             probingRecords.add(new MdnsKeyRecord(
541                             keyRecord.getName(),
542                             0L /* receiptTimeMillis */,
543                             false /* cacheFlush */,
544                             keyRecord.getTtl(),
545                             keyRecord.getRData()));
546         }
547         return new MdnsProber.ProbingInfo(serviceId, probingRecords);
548     }
549 
attrsToTextEntries(Map<String, byte[]> attrs)550     private static List<MdnsServiceInfo.TextEntry> attrsToTextEntries(Map<String, byte[]> attrs) {
551         final List<MdnsServiceInfo.TextEntry> out = new ArrayList<>(attrs.size());
552         for (Map.Entry<String, byte[]> attr : attrs.entrySet()) {
553             out.add(new MdnsServiceInfo.TextEntry(attr.getKey(), attr.getValue()));
554         }
555         return out;
556     }
557 
558     /**
559      * Mark a service in the repository as exiting.
560      * @param id ID of the service, used at registration time.
561      * @return The exit announcement to indicate the service was removed, or null if not necessary.
562      */
563     @Nullable
exitService(int id)564     public MdnsAnnouncer.ExitAnnouncementInfo exitService(int id) {
565         final ServiceRegistration registration = mServices.get(id);
566         if (registration == null) return null;
567         if (registration.exiting) return null;
568 
569         // Send exit (TTL 0) for the PTR records, if at least one was sent (in particular don't send
570         // if still probing)
571         if (CollectionUtils.all(registration.ptrRecords, r -> r.lastSentTimeMs == 0L)) {
572             return null;
573         }
574 
575         registration.exiting = true;
576         final List<MdnsRecord> expiredRecords = CollectionUtils.map(registration.ptrRecords,
577                 r -> new MdnsPointerRecord(
578                         r.record.getName(),
579                         0L /* receiptTimeMillis */,
580                         // RFC6762#10.1, the cache flush bit should be false for existing
581                         // announcement. Otherwise, the record will be deleted immediately.
582                         false /* cacheFlush */,
583                         0L /* ttlMillis */,
584                         r.record.getPointer()));
585 
586         // Exit should be skipped if the record is still advertised by another service, but that
587         // would be a conflict (2 service registrations with the same service name), so it would
588         // not have been allowed by the repository.
589         return new MdnsAnnouncer.ExitAnnouncementInfo(id, expiredRecords);
590     }
591 
removeService(int id)592     public void removeService(int id) {
593         mServices.remove(id);
594     }
595 
596     /**
597      * @return The number of services currently held in the repository, including exiting services.
598      */
getServicesCount()599     public int getServicesCount() {
600         return mServices.size();
601     }
602 
603     /**
604      * @return The replied request count of the service.
605      */
getServiceRepliedRequestsCount(int id)606     public int getServiceRepliedRequestsCount(int id) {
607         final ServiceRegistration service = mServices.get(id);
608         if (service == null) return NO_PACKET;
609         return service.repliedServiceCount;
610     }
611 
612     /**
613      * @return The total sent packet count of the service.
614      */
getSentPacketCount(int id)615     public int getSentPacketCount(int id) {
616         final ServiceRegistration service = mServices.get(id);
617         if (service == null) return NO_PACKET;
618         return service.sentPacketCount;
619     }
620 
621     /**
622      * Remove all services from the repository
623      * @return IDs of the removed services
624      */
625     @NonNull
clearServices()626     public int[] clearServices() {
627         final int[] ret = new int[mServices.size()];
628         for (int i = 0; i < mServices.size(); i++) {
629             ret[i] = mServices.keyAt(i);
630         }
631         mServices.clear();
632         return ret;
633     }
634 
isTruncatedKnownAnswerPacket(MdnsPacket packet)635     private boolean isTruncatedKnownAnswerPacket(MdnsPacket packet) {
636         if (!mMdnsFeatureFlags.isKnownAnswerSuppressionEnabled()
637                 // Should ignore the response packet.
638                 || (packet.flags & MdnsConstants.FLAGS_RESPONSE) != 0) {
639             return false;
640         }
641         // Check the packet contains no questions and as many more Known-Answer records as will fit.
642         return packet.questions.size() == 0 && packet.answers.size() != 0;
643     }
644 
645     /**
646      * Get the reply to send to an incoming packet.
647      *
648      * @param packet The incoming packet.
649      * @param src The source address of the incoming packet.
650      */
651     @Nullable
getReply(MdnsPacket packet, InetSocketAddress src)652     public MdnsReplyInfo getReply(MdnsPacket packet, InetSocketAddress src) {
653         final long now = mDeps.elapsedRealTime();
654         final boolean isQuestionOnIpv4 = src.getAddress() instanceof Inet4Address;
655 
656         // TODO: b/322142420 - Set<RecordInfo<?>> may contain duplicate records wrapped in different
657         // RecordInfo<?>s when custom host is enabled.
658 
659         // Use LinkedHashSet for preserving the insert order of the RRs, so that RRs of the same
660         // service or host are grouped together (which is more developer-friendly).
661         final Set<RecordInfo<?>> answerInfo = new LinkedHashSet<>();
662         final Set<RecordInfo<?>> additionalAnswerInfo = new LinkedHashSet<>();
663         // Reply unicast if the feature is enabled AND all replied questions request unicast
664         final boolean replyUnicastEnabled = mMdnsFeatureFlags.isUnicastReplyEnabled();
665         boolean replyUnicast = replyUnicastEnabled;
666         for (MdnsRecord question : packet.questions) {
667             // Add answers from general records
668             if (addReplyFromService(question, mGeneralRecords, null /* servicePtrRecord */,
669                     null /* serviceSrvRecord */, null /* serviceTxtRecord */,
670                     null /* hostname */,
671                     replyUnicastEnabled, now, answerInfo, additionalAnswerInfo,
672                     Collections.emptyList(), isQuestionOnIpv4)) {
673                 replyUnicast &= question.isUnicastReplyRequested();
674             }
675 
676             // Add answers from each service
677             for (int i = 0; i < mServices.size(); i++) {
678                 final ServiceRegistration registration = mServices.valueAt(i);
679                 if (registration.exiting || registration.isProbing) continue;
680                 if (addReplyFromService(question, registration.allRecords, registration.ptrRecords,
681                         registration.srvRecord, registration.txtRecord,
682                         registration.serviceInfo.getHostname(),
683                         replyUnicastEnabled, now,
684                         answerInfo, additionalAnswerInfo, packet.answers, isQuestionOnIpv4)) {
685                     replyUnicast &= question.isUnicastReplyRequested();
686                     registration.repliedServiceCount++;
687                     registration.sentPacketCount++;
688                 }
689             }
690         }
691 
692         // If any record was already in the answer section, remove it from the additional answer
693         // section. This can typically happen when there are both queries for
694         // SRV / TXT / A / AAAA and PTR (which can cause SRV / TXT / A / AAAA records being added
695         // to the additional answer section).
696         additionalAnswerInfo.removeAll(answerInfo);
697 
698         final List<MdnsRecord> additionalAnswerRecords =
699                 new ArrayList<>(additionalAnswerInfo.size());
700         for (RecordInfo<?> info : additionalAnswerInfo) {
701             // Different RecordInfos may contain the same record.
702             // For example, when there are multiple services referring to the same custom host,
703             // there are multiple RecordInfos containing the same address record.
704             if (!additionalAnswerRecords.contains(info.record)) {
705                 additionalAnswerRecords.add(info.record);
706             }
707         }
708 
709         // RFC6762 6.1: negative responses
710         // "On receipt of a question for a particular name, rrtype, and rrclass, for which a
711         // responder does have one or more unique answers, the responder MAY also include an NSEC
712         // record in the Additional Record Section indicating the nonexistence of other rrtypes
713         // for that name and rrclass."
714         addNsecRecordsForUniqueNames(additionalAnswerRecords,
715                 answerInfo.iterator(), additionalAnswerInfo.iterator());
716 
717         if (answerInfo.size() == 0 && additionalAnswerRecords.size() == 0) {
718             // RFC6762 7.2. Multipacket Known-Answer Suppression
719             // Sometimes a Multicast DNS querier will already have too many answers
720             // to fit in the Known-Answer Section of its query packets. In this
721             // case, it should issue a Multicast DNS query containing a question and
722             // as many Known-Answer records as will fit.  It MUST then set the TC
723             // (Truncated) bit in the header before sending the query.  It MUST
724             // immediately follow the packet with another query packet containing no
725             // questions and as many more Known-Answer records as will fit.  If
726             // there are still too many records remaining to fit in the packet, it
727             // again sets the TC bit and continues until all the Known-Answer
728             // records have been sent.
729             if (!isTruncatedKnownAnswerPacket(packet)) {
730                 return null;
731             }
732         }
733 
734         // Determine the send delay
735         final long delayMs;
736         if ((packet.flags & MdnsConstants.FLAG_TRUNCATED) != 0) {
737             // RFC 6762 6.: 400-500ms delay if TC bit is set
738             delayMs = 400L + mDelayGenerator.nextInt(100);
739         } else if (packet.questions.size() > 1
740                 || CollectionUtils.any(answerInfo, a -> a.isSharedName)) {
741             // 20-120ms if there may be responses from other hosts (not a fully owned
742             // name) (RFC 6762 6.), or if there are multiple questions (6.3).
743             // TODO: this should be 0 if this is a probe query ("can be distinguished from a
744             // normal query by the fact that a probe query contains a proposed record in the
745             // Authority Section that answers the question" in 6.), and the reply is for a fully
746             // owned record.
747             delayMs = 20L + mDelayGenerator.nextInt(100);
748         } else {
749             delayMs = 0L;
750         }
751 
752         // Determine the send destination
753         final InetSocketAddress dest;
754         if (replyUnicast) {
755             // As per RFC6762 5.4, "if the responder has not multicast that record recently (within
756             // one quarter of its TTL), then the responder SHOULD instead multicast the response so
757             // as to keep all the peer caches up to date": this SHOULD is not implemented to
758             // minimize latency for queriers who have just started, so they did not receive previous
759             // multicast responses. Unicast replies are faster as they do not need to wait for the
760             // beacon interval on Wi-Fi.
761             dest = src;
762         } else if (isQuestionOnIpv4) {
763             dest = IPV4_SOCKET_ADDR;
764         } else {
765             dest = IPV6_SOCKET_ADDR;
766         }
767 
768         // Build the list of answer records from their RecordInfo
769         final ArrayList<MdnsRecord> answerRecords = new ArrayList<>(answerInfo.size());
770         for (RecordInfo<?> info : answerInfo) {
771             // TODO: consider actual packet send delay after response aggregation
772             info.lastSentTimeMs = now + delayMs;
773             if (!replyUnicast) {
774                 if (isQuestionOnIpv4) {
775                     info.lastAdvertisedOnIpv4TimeMs = info.lastSentTimeMs;
776                 } else {
777                     info.lastAdvertisedOnIpv6TimeMs = info.lastSentTimeMs;
778                 }
779             }
780             // Different RecordInfos may the contain the same record
781             if (!answerRecords.contains(info.record)) {
782                 answerRecords.add(info.record);
783             }
784         }
785 
786         return new MdnsReplyInfo(answerRecords, additionalAnswerRecords, delayMs, dest, src,
787                 new ArrayList<>(packet.answers));
788     }
789 
isKnownAnswer(MdnsRecord answer, @NonNull List<MdnsRecord> knownAnswerRecords)790     private boolean isKnownAnswer(MdnsRecord answer, @NonNull List<MdnsRecord> knownAnswerRecords) {
791         for (MdnsRecord knownAnswer : knownAnswerRecords) {
792             if (answer.equals(knownAnswer) && knownAnswer.getTtl() > (answer.getTtl() / 2)) {
793                 return true;
794             }
795         }
796         return false;
797     }
798 
799     /**
800      * Add answers and additional answers for a question, from a ServiceRegistration.
801      */
addReplyFromService(@onNull MdnsRecord question, @NonNull List<RecordInfo<?>> serviceRecords, @Nullable List<RecordInfo<MdnsPointerRecord>> servicePtrRecords, @Nullable RecordInfo<MdnsServiceRecord> serviceSrvRecord, @Nullable RecordInfo<MdnsTextRecord> serviceTxtRecord, @Nullable String hostname, boolean replyUnicastEnabled, long now, @NonNull Set<RecordInfo<?>> answerInfo, @NonNull Set<RecordInfo<?>> additionalAnswerInfo, @NonNull List<MdnsRecord> knownAnswerRecords, boolean isQuestionOnIpv4)802     private boolean addReplyFromService(@NonNull MdnsRecord question,
803             @NonNull List<RecordInfo<?>> serviceRecords,
804             @Nullable List<RecordInfo<MdnsPointerRecord>> servicePtrRecords,
805             @Nullable RecordInfo<MdnsServiceRecord> serviceSrvRecord,
806             @Nullable RecordInfo<MdnsTextRecord> serviceTxtRecord,
807             @Nullable String hostname,
808             boolean replyUnicastEnabled, long now, @NonNull Set<RecordInfo<?>> answerInfo,
809             @NonNull Set<RecordInfo<?>> additionalAnswerInfo,
810             @NonNull List<MdnsRecord> knownAnswerRecords,
811             boolean isQuestionOnIpv4) {
812         boolean hasDnsSdPtrRecordAnswer = false;
813         boolean hasDnsSdSrvRecordAnswer = false;
814         boolean hasFullyOwnedNameMatch = false;
815         boolean hasKnownAnswer = false;
816 
817         final int answersStartSize = answerInfo.size();
818         for (RecordInfo<?> info : serviceRecords) {
819 
820              /* RFC6762 6.: the record name must match the question name, the record rrtype
821              must match the question qtype unless the qtype is "ANY" (255) or the rrtype is
822              "CNAME" (5), and the record rrclass must match the question qclass unless the
823              qclass is "ANY" (255) */
824             if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(info.record.getName(), question.getName())) {
825                 continue;
826             }
827             hasFullyOwnedNameMatch |= !info.isSharedName;
828 
829             // The repository does not store CNAME records
830             if (question.getType() != MdnsRecord.TYPE_ANY
831                     && question.getType() != info.record.getType()) {
832                 continue;
833             }
834             if (question.getRecordClass() != MdnsRecord.CLASS_ANY
835                     && question.getRecordClass() != info.record.getRecordClass()) {
836                 continue;
837             }
838 
839             hasKnownAnswer = true;
840 
841             // RFC6762 7.1. Known-Answer Suppression:
842             // A Multicast DNS responder MUST NOT answer a Multicast DNS query if
843             // the answer it would give is already included in the Answer Section
844             // with an RR TTL at least half the correct value.  If the RR TTL of the
845             // answer as given in the Answer Section is less than half of the true
846             // RR TTL as known by the Multicast DNS responder, the responder MUST
847             // send an answer so as to update the querier's cache before the record
848             // becomes in danger of expiration.
849             if (mMdnsFeatureFlags.isKnownAnswerSuppressionEnabled()
850                     && isKnownAnswer(info.record, knownAnswerRecords)) {
851                 continue;
852             }
853 
854             hasDnsSdPtrRecordAnswer |= (servicePtrRecords != null
855                     && CollectionUtils.any(servicePtrRecords, r -> info == r));
856             hasDnsSdSrvRecordAnswer |= (info == serviceSrvRecord);
857 
858             // TODO: responses to probe queries should bypass this check and only ensure the
859             // reply is sent 250ms after the last sent time (RFC 6762 p.15)
860             if (!(replyUnicastEnabled && question.isUnicastReplyRequested())) {
861                 if (isQuestionOnIpv4) { // IPv4
862                     if (info.lastAdvertisedOnIpv4TimeMs > 0L
863                             && now - info.lastAdvertisedOnIpv4TimeMs
864                                     < MIN_MULTICAST_REPLY_INTERVAL_MS) {
865                         continue;
866                     }
867                 } else { // IPv6
868                     if (info.lastAdvertisedOnIpv6TimeMs > 0L
869                             && now - info.lastAdvertisedOnIpv6TimeMs
870                                     < MIN_MULTICAST_REPLY_INTERVAL_MS) {
871                         continue;
872                     }
873                 }
874             }
875 
876             answerInfo.add(info);
877         }
878 
879         // RFC6762 6.1:
880         // "Any time a responder receives a query for a name for which it has verified exclusive
881         // ownership, for a type for which that name has no records, the responder MUST [...]
882         // respond asserting the nonexistence of that record"
883         if (hasFullyOwnedNameMatch && !hasKnownAnswer) {
884             MdnsNsecRecord nsecRecord = new MdnsNsecRecord(
885                     question.getName(),
886                     0L /* receiptTimeMillis */,
887                     true /* cacheFlush */,
888                     // TODO: RFC6762 6.1: "In general, the TTL given for an NSEC record SHOULD
889                     // be the same as the TTL that the record would have had, had it existed."
890                     DEFAULT_NAME_RECORDS_TTL_MILLIS,
891                     question.getName(),
892                     new int[] { question.getType() });
893             additionalAnswerInfo.add(
894                     new RecordInfo<>(null /* serviceInfo */, nsecRecord, false /* isSharedName */));
895         }
896 
897         // No more records to add if no answer
898         if (answerInfo.size() == answersStartSize) return false;
899 
900         // RFC6763 12.1: if including PTR record, include the SRV and TXT records it names
901         if (hasDnsSdPtrRecordAnswer) {
902             if (serviceTxtRecord != null) {
903                 additionalAnswerInfo.add(serviceTxtRecord);
904             }
905             if (serviceSrvRecord != null) {
906                 additionalAnswerInfo.add(serviceSrvRecord);
907             }
908         }
909 
910         // RFC6763 12.1&.2: if including PTR or SRV record, include the address records it names
911         if (hasDnsSdPtrRecordAnswer || hasDnsSdSrvRecordAnswer) {
912             additionalAnswerInfo.addAll(getInetAddressRecordsForHostname(hostname));
913         }
914         return true;
915     }
916 
917     /**
918      * Add NSEC records indicating that the response records are unique.
919      *
920      * Following RFC6762 6.1:
921      * "On receipt of a question for a particular name, rrtype, and rrclass, for which a responder
922      * does have one or more unique answers, the responder MAY also include an NSEC record in the
923      * Additional Record Section indicating the nonexistence of other rrtypes for that name and
924      * rrclass."
925      * @param destinationList List to add the NSEC records to.
926      * @param answerRecords Lists of answered records based on which to add NSEC records (typically
927      *                      answer and additionalAnswer sections)
928      */
929     @SafeVarargs
addNsecRecordsForUniqueNames( List<MdnsRecord> destinationList, Iterator<RecordInfo<?>>... answerRecords)930     private void addNsecRecordsForUniqueNames(
931             List<MdnsRecord> destinationList,
932             Iterator<RecordInfo<?>>... answerRecords) {
933         // Group unique records by name. Use a TreeMap with comparator as arrays don't implement
934         // equals / hashCode.
935         final Map<String[], List<MdnsRecord>> nsecByName = new TreeMap<>(Arrays::compare);
936         // But keep the list of names in added order, otherwise records would be sorted in
937         // alphabetical order instead of the order of the original records, which would look like
938         // inconsistent behavior depending on service name.
939         final List<String[]> namesInAddedOrder = new ArrayList<>();
940         for (Iterator<RecordInfo<?>> answers : answerRecords) {
941             addNonSharedRecordsToMap(answers, nsecByName, namesInAddedOrder);
942         }
943 
944         for (String[] nsecName : namesInAddedOrder) {
945             final List<MdnsRecord> entryRecords = nsecByName.get(nsecName);
946 
947             // Add NSEC records only when the answers include all unique records of this name
948             if (entryRecords.size() != countUniqueRecords(nsecName)) {
949                 continue;
950             }
951 
952             long minTtl = Long.MAX_VALUE;
953             final Set<Integer> types = new ArraySet<>(entryRecords.size());
954             for (MdnsRecord record : entryRecords) {
955                 if (minTtl > record.getTtl()) minTtl = record.getTtl();
956                 types.add(record.getType());
957             }
958 
959             destinationList.add(new MdnsNsecRecord(
960                     nsecName,
961                     0L /* receiptTimeMillis */,
962                     true /* cacheFlush */,
963                     minTtl,
964                     nsecName,
965                     CollectionUtils.toIntArray(types)));
966         }
967     }
968 
969     /** Returns the number of unique records on this device for a given {@code name}. */
countUniqueRecords(String[] name)970     private int countUniqueRecords(String[] name) {
971         int cnt = countUniqueRecords(mGeneralRecords, name);
972 
973         for (int i = 0; i < mServices.size(); i++) {
974             final ServiceRegistration registration = mServices.valueAt(i);
975             cnt += countUniqueRecords(registration.allRecords, name);
976         }
977         return cnt;
978     }
979 
countUniqueRecords(List<RecordInfo<?>> records, String[] name)980     private static int countUniqueRecords(List<RecordInfo<?>> records, String[] name) {
981         int cnt = 0;
982         for (RecordInfo<?> record : records) {
983             if (!record.isSharedName && Arrays.equals(name, record.record.getName())) {
984                 cnt++;
985             }
986         }
987         return cnt;
988     }
989 
990     /**
991      * Add non-shared records to a map listing them by record name, and to a list of names that
992      * remembers the adding order.
993      *
994      * In the destination map records are grouped by name; so the map has one key per record name,
995      * and the values are the lists of different records that share the same name.
996      * @param records Records to scan.
997      * @param dest Map to add the records to.
998      * @param namesInAddedOrder List of names to add the names in order, keeping the first
999      *                          occurrence of each name.
1000      */
addNonSharedRecordsToMap( Iterator<RecordInfo<?>> records, Map<String[], List<MdnsRecord>> dest, @Nullable List<String[]> namesInAddedOrder)1001     private static void addNonSharedRecordsToMap(
1002             Iterator<RecordInfo<?>> records,
1003             Map<String[], List<MdnsRecord>> dest,
1004             @Nullable List<String[]> namesInAddedOrder) {
1005         while (records.hasNext()) {
1006             final RecordInfo<?> record = records.next();
1007             if (record.isSharedName || record.record instanceof MdnsNsecRecord) continue;
1008             final List<MdnsRecord> recordsForName = dest.computeIfAbsent(record.record.name,
1009                     key -> {
1010                         namesInAddedOrder.add(key);
1011                         return new ArrayList<>();
1012                     });
1013             recordsForName.add(record.record);
1014         }
1015     }
1016 
1017     @Nullable
getHostnameForServiceId(int id)1018     public String getHostnameForServiceId(int id) {
1019         ServiceRegistration registration = mServices.get(id);
1020         if (registration == null) {
1021             return null;
1022         }
1023         return registration.serviceInfo.getHostname();
1024     }
1025 
1026     /**
1027      * Restart probing the services which are being probed and using the given custom hostname.
1028      *
1029      * @return The list of {@link MdnsProber.ProbingInfo} to be used by advertiser.
1030      */
restartProbingForHostname(@onNull String hostname)1031     public List<MdnsProber.ProbingInfo> restartProbingForHostname(@NonNull String hostname) {
1032         final ArrayList<MdnsProber.ProbingInfo> probingInfos = new ArrayList<>();
1033         forEachActiveServiceRegistrationWithHostname(
1034                 hostname,
1035                 (id, registration) -> {
1036                     if (!registration.isProbing) {
1037                         return;
1038                     }
1039                     probingInfos.add(makeProbingInfo(id, registration));
1040                 });
1041         return probingInfos;
1042     }
1043 
1044     /**
1045      * Restart announcing the services which are using the given custom hostname.
1046      *
1047      * @return The list of {@link MdnsAnnouncer.AnnouncementInfo} to be used by advertiser.
1048      */
restartAnnouncingForHostname( @onNull String hostname)1049     public List<MdnsAnnouncer.AnnouncementInfo> restartAnnouncingForHostname(
1050             @NonNull String hostname) {
1051         final ArrayList<MdnsAnnouncer.AnnouncementInfo> announcementInfos = new ArrayList<>();
1052         forEachActiveServiceRegistrationWithHostname(
1053                 hostname,
1054                 (id, registration) -> {
1055                     if (registration.isProbing) {
1056                         return;
1057                     }
1058                     announcementInfos.add(makeAnnouncementInfo(id, registration));
1059                 });
1060         return announcementInfos;
1061     }
1062 
1063     /**
1064      * Called to indicate that probing succeeded for a service.
1065      *
1066      * @param probeSuccessInfo The successful probing info.
1067      * @return The {@link MdnsAnnouncer.AnnouncementInfo} to send, now that probing has succeeded.
1068      */
onProbingSucceeded( MdnsProber.ProbingInfo probeSuccessInfo)1069     public MdnsAnnouncer.AnnouncementInfo onProbingSucceeded(
1070             MdnsProber.ProbingInfo probeSuccessInfo) throws IOException {
1071         final int serviceId = probeSuccessInfo.getServiceId();
1072         final ServiceRegistration registration = mServices.get(serviceId);
1073         if (registration == null) {
1074             throw new IOException("Service is not registered: " + serviceId);
1075         }
1076         registration.setProbing(false);
1077 
1078         return makeAnnouncementInfo(serviceId, registration);
1079     }
1080 
1081     /**
1082      * Make the announcement info of the given service ID.
1083      *
1084      * @param serviceId The service ID.
1085      * @param registration The service registration.
1086      * @return The {@link MdnsAnnouncer.AnnouncementInfo} of the given service ID.
1087      */
makeAnnouncementInfo( int serviceId, ServiceRegistration registration)1088     private MdnsAnnouncer.AnnouncementInfo makeAnnouncementInfo(
1089             int serviceId, ServiceRegistration registration) {
1090         final Set<MdnsRecord> answersSet = new LinkedHashSet<>();
1091         final ArrayList<MdnsRecord> additionalAnswers = new ArrayList<>();
1092 
1093         // When using default host, add interface address records from general records
1094         if (TextUtils.isEmpty(registration.serviceInfo.getHostname())) {
1095             for (RecordInfo<?> record : mGeneralRecords) {
1096                 answersSet.add(record.record);
1097             }
1098         } else {
1099             // TODO: b/321617573 - include PTR records for addresses
1100             // The custom host may have more addresses in other registrations
1101             forEachActiveServiceRegistrationWithHostname(
1102                     registration.serviceInfo.getHostname(),
1103                     (id, otherRegistration) -> {
1104                         if (otherRegistration.isProbing) {
1105                             return;
1106                         }
1107                         for (RecordInfo<?> addressRecordInfo : otherRegistration.addressRecords) {
1108                             answersSet.add(addressRecordInfo.record);
1109                         }
1110                     });
1111         }
1112 
1113         // All service records
1114         for (RecordInfo<?> info : registration.allRecords) {
1115             answersSet.add(info.record);
1116         }
1117 
1118         addNsecRecordsForUniqueNames(additionalAnswers,
1119                 mGeneralRecords.iterator(), registration.allRecords.iterator());
1120 
1121         return new MdnsAnnouncer.AnnouncementInfo(serviceId,
1122                 new ArrayList<>(answersSet), additionalAnswers);
1123     }
1124 
1125     /**
1126      * Gets the offload MdnsPacket.
1127      * @param serviceId The serviceId.
1128      * @return The offload {@link MdnsPacket} that contains PTR/SRV/TXT/A/AAAA records.
1129      */
getOffloadPacket(int serviceId)1130     public MdnsPacket getOffloadPacket(int serviceId) throws IllegalArgumentException {
1131         final ServiceRegistration registration = mServices.get(serviceId);
1132         if (registration == null) throw new IllegalArgumentException(
1133                 "Service is not registered: " + serviceId);
1134 
1135         final ArrayList<MdnsRecord> answers = new ArrayList<>();
1136 
1137         // Adds all PTR, SRV, TXT, A/AAAA records.
1138         for (RecordInfo<MdnsPointerRecord> ptrRecord : registration.ptrRecords) {
1139             answers.add(ptrRecord.record);
1140         }
1141         if (registration.srvRecord != null) {
1142             answers.add(registration.srvRecord.record);
1143         }
1144         if (registration.txtRecord != null) {
1145             answers.add(registration.txtRecord.record);
1146         }
1147         // TODO: Support custom host. It currently only supports default host.
1148         for (RecordInfo<?> record : mGeneralRecords) {
1149             if (record.record instanceof MdnsInetAddressRecord) {
1150                 answers.add(record.record);
1151             }
1152         }
1153 
1154         final int flags = 0x8400; // Response, authoritative (rfc6762 18.4)
1155         return new MdnsPacket(flags,
1156                 Collections.emptyList() /* questions */,
1157                 answers,
1158                 Collections.emptyList() /* authorityRecords */,
1159                 Collections.emptyList() /* additionalRecords */);
1160     }
1161 
1162     /** Check if the record is in a registration */
hasInetAddressRecord( @onNull ServiceRegistration registration, @NonNull MdnsInetAddressRecord record)1163     private static boolean hasInetAddressRecord(
1164             @NonNull ServiceRegistration registration, @NonNull MdnsInetAddressRecord record) {
1165         for (RecordInfo<MdnsInetAddressRecord> localRecord : registration.addressRecords) {
1166             if (Objects.equals(localRecord.record, record)) {
1167                 return true;
1168             }
1169         }
1170 
1171         return false;
1172     }
1173 
1174     /**
1175      * Get the service IDs of services conflicting with a received packet.
1176      *
1177      * <p>It returns a Map of service ID => conflict type. Conflict type is a bitmap telling which
1178      * part of the service is conflicting. See {@link MdnsInterfaceAdvertiser#CONFLICT_SERVICE} and
1179      * {@link MdnsInterfaceAdvertiser#CONFLICT_HOST}.
1180      */
getConflictingServices(MdnsPacket packet)1181     public Map<Integer, Integer> getConflictingServices(MdnsPacket packet) {
1182         Map<Integer, Integer> conflicting = new ArrayMap<>();
1183         for (MdnsRecord record : packet.answers) {
1184             SparseIntArray conflictingWithRecord = new SparseIntArray();
1185             for (int i = 0; i < mServices.size(); i++) {
1186                 final ServiceRegistration registration = mServices.valueAt(i);
1187                 if (registration.exiting) continue;
1188 
1189                 final RecordConflictType conflictForService =
1190                         conflictForService(record, registration);
1191                 final RecordConflictType conflictForHost = conflictForHost(record, registration);
1192 
1193                 // Identical record is found in the repository so there won't be a conflict.
1194                 if (conflictForService == RecordConflictType.IDENTICAL
1195                         || conflictForHost == RecordConflictType.IDENTICAL) {
1196                     conflictingWithRecord.clear();
1197                     break;
1198                 }
1199 
1200                 int conflictType = 0;
1201                 if (conflictForService == RecordConflictType.CONFLICT) {
1202                     conflictType |= CONFLICT_SERVICE;
1203                 }
1204                 if (conflictForHost == RecordConflictType.CONFLICT) {
1205                     conflictType |= CONFLICT_HOST;
1206                 }
1207 
1208                 if (conflictType != 0) {
1209                     final int serviceId = mServices.keyAt(i);
1210                     conflictingWithRecord.put(serviceId, conflictType);
1211                 }
1212             }
1213             for (int i = 0; i < conflictingWithRecord.size(); i++) {
1214                 final int serviceId = conflictingWithRecord.keyAt(i);
1215                 final int conflictType = conflictingWithRecord.valueAt(i);
1216                 final int oldConflictType = conflicting.getOrDefault(serviceId, 0);
1217                 conflicting.put(serviceId, oldConflictType | conflictType);
1218             }
1219         }
1220 
1221         return conflicting;
1222     }
1223 
conflictForService( @onNull MdnsRecord record, @NonNull ServiceRegistration registration)1224     private static RecordConflictType conflictForService(
1225             @NonNull MdnsRecord record, @NonNull ServiceRegistration registration) {
1226         String[] fullServiceName;
1227         if (registration.srvRecord != null) {
1228             fullServiceName = registration.srvRecord.record.getName();
1229         } else if (registration.serviceKeyRecord != null) {
1230             fullServiceName = registration.serviceKeyRecord.record.getName();
1231         } else {
1232             return RecordConflictType.NO_CONFLICT;
1233         }
1234 
1235         if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(record.getName(), fullServiceName)) {
1236             return RecordConflictType.NO_CONFLICT;
1237         }
1238 
1239         // As per RFC6762 9., it's fine if the "conflict" is an identical record with same
1240         // data.
1241         if (record instanceof MdnsServiceRecord && equals(record, registration.srvRecord)) {
1242             return RecordConflictType.IDENTICAL;
1243         }
1244         if (record instanceof MdnsTextRecord && equals(record, registration.txtRecord)) {
1245             return RecordConflictType.IDENTICAL;
1246         }
1247         if (record instanceof MdnsKeyRecord && equals(record, registration.serviceKeyRecord)) {
1248             return RecordConflictType.IDENTICAL;
1249         }
1250 
1251         return RecordConflictType.CONFLICT;
1252     }
1253 
conflictForHost( @onNull MdnsRecord record, @NonNull ServiceRegistration registration)1254     private RecordConflictType conflictForHost(
1255             @NonNull MdnsRecord record, @NonNull ServiceRegistration registration) {
1256         // Only custom hosts are checked. When using the default host, the hostname is derived from
1257         // a UUID and it's supposed to be unique.
1258         if (registration.serviceInfo.getHostname() == null) {
1259             return RecordConflictType.NO_CONFLICT;
1260         }
1261 
1262         // It cannot be a hostname conflict because no record is registered with the hostname.
1263         if (registration.addressRecords.isEmpty() && registration.hostKeyRecord == null) {
1264             return RecordConflictType.NO_CONFLICT;
1265         }
1266 
1267         // The record's name cannot be registered by NsdManager so it's not a conflict.
1268         if (record.getName().length != 2 || !record.getName()[1].equals(LOCAL_TLD)) {
1269             return RecordConflictType.NO_CONFLICT;
1270         }
1271 
1272         // Different names. There won't be a conflict.
1273         if (!MdnsUtils.equalsIgnoreDnsCase(
1274                 record.getName()[0], registration.serviceInfo.getHostname())) {
1275             return RecordConflictType.NO_CONFLICT;
1276         }
1277 
1278         // As per RFC6762 9., it's fine if the "conflict" is an identical record with same
1279         // data.
1280         if (record instanceof MdnsInetAddressRecord
1281                 && hasInetAddressRecord(registration, (MdnsInetAddressRecord) record)) {
1282             return RecordConflictType.IDENTICAL;
1283         }
1284         if (record instanceof MdnsKeyRecord && equals(record, registration.hostKeyRecord)) {
1285             return RecordConflictType.IDENTICAL;
1286         }
1287 
1288         // Per RFC 6762 8.1, when a record is being probed, any answer containing a record with that
1289         // name, of any type, MUST be considered a conflicting response.
1290         if (registration.isProbing) {
1291             return RecordConflictType.CONFLICT;
1292         }
1293         if (record instanceof MdnsInetAddressRecord && !registration.addressRecords.isEmpty()) {
1294             return RecordConflictType.CONFLICT;
1295         }
1296         if (record instanceof MdnsKeyRecord && registration.hostKeyRecord != null) {
1297             return RecordConflictType.CONFLICT;
1298         }
1299 
1300         return RecordConflictType.NO_CONFLICT;
1301     }
1302 
getInetAddressRecordsForHostname( @ullable String hostname)1303     private List<RecordInfo<MdnsInetAddressRecord>> getInetAddressRecordsForHostname(
1304             @Nullable String hostname) {
1305         List<RecordInfo<MdnsInetAddressRecord>> records = new ArrayList<>();
1306         if (TextUtils.isEmpty(hostname)) {
1307             forEachAddressRecord(mGeneralRecords, records::add);
1308         } else {
1309             forEachActiveServiceRegistrationWithHostname(
1310                     hostname,
1311                     (id, service) -> {
1312                         if (service.isProbing) return;
1313                         records.addAll(service.addressRecords);
1314                     });
1315         }
1316         return records;
1317     }
1318 
makeProbingInetAddressRecords( @onNull NsdServiceInfo serviceInfo)1319     private List<MdnsInetAddressRecord> makeProbingInetAddressRecords(
1320             @NonNull NsdServiceInfo serviceInfo) {
1321         final List<MdnsInetAddressRecord> records = new ArrayList<>();
1322         if (TextUtils.isEmpty(serviceInfo.getHostname())) {
1323             if (mMdnsFeatureFlags.mIncludeInetAddressRecordsInProbing) {
1324                 forEachAddressRecord(mGeneralRecords, r -> records.add(r.record));
1325             }
1326         } else {
1327             forEachActiveServiceRegistrationWithHostname(
1328                     serviceInfo.getHostname(),
1329                     (id, service) -> {
1330                         for (RecordInfo<MdnsInetAddressRecord> recordInfo :
1331                                 service.addressRecords) {
1332                             records.add(recordInfo.record);
1333                         }
1334                     });
1335         }
1336         return records;
1337     }
1338 
forEachAddressRecord( List<RecordInfo<?>> records, Consumer<RecordInfo<MdnsInetAddressRecord>> consumer)1339     private static void forEachAddressRecord(
1340             List<RecordInfo<?>> records, Consumer<RecordInfo<MdnsInetAddressRecord>> consumer) {
1341         for (RecordInfo<?> record : records) {
1342             if (record.record instanceof MdnsInetAddressRecord) {
1343                 consumer.accept((RecordInfo<MdnsInetAddressRecord>) record);
1344             }
1345         }
1346     }
1347 
forEachActiveServiceRegistrationWithHostname( @onNull String hostname, BiConsumer<Integer, ServiceRegistration> consumer)1348     private void forEachActiveServiceRegistrationWithHostname(
1349             @NonNull String hostname, BiConsumer<Integer, ServiceRegistration> consumer) {
1350         for (int i = 0; i < mServices.size(); ++i) {
1351             int id = mServices.keyAt(i);
1352             ServiceRegistration service = mServices.valueAt(i);
1353             if (service.exiting) continue;
1354             if (MdnsUtils.equalsIgnoreDnsCase(service.serviceInfo.getHostname(), hostname)) {
1355                 consumer.accept(id, service);
1356             }
1357         }
1358     }
1359 
1360     /**
1361      * (Re)set a service to the probing state.
1362      * @return The {@link MdnsProber.ProbingInfo} to send for probing.
1363      */
1364     @Nullable
setServiceProbing(int serviceId)1365     public MdnsProber.ProbingInfo setServiceProbing(int serviceId) {
1366         final ServiceRegistration registration = mServices.get(serviceId);
1367         if (registration == null) return null;
1368 
1369         registration.setProbing(true);
1370 
1371         return makeProbingInfo(serviceId, registration);
1372     }
1373 
1374     /**
1375      * Indicates whether a given service is in probing state.
1376      */
isProbing(int serviceId)1377     public boolean isProbing(int serviceId) {
1378         final ServiceRegistration registration = mServices.get(serviceId);
1379         if (registration == null) return false;
1380 
1381         return registration.isProbing;
1382     }
1383 
1384     /**
1385      * Return whether the repository has an active (non-exiting) service for the given ID.
1386      */
hasActiveService(int serviceId)1387     public boolean hasActiveService(int serviceId) {
1388         final ServiceRegistration registration = mServices.get(serviceId);
1389         if (registration == null) return false;
1390 
1391         return !registration.exiting;
1392     }
1393 
1394     /**
1395      * Rename a service to the newly provided info, following a conflict.
1396      *
1397      * If the specified service does not exist, this returns null.
1398      */
1399     @Nullable
renameServiceForConflict(int serviceId, NsdServiceInfo newInfo)1400     public MdnsProber.ProbingInfo renameServiceForConflict(int serviceId, NsdServiceInfo newInfo) {
1401         final ServiceRegistration existing = mServices.get(serviceId);
1402         if (existing == null) return null;
1403 
1404         final ServiceRegistration newService = new ServiceRegistration(mDeviceHostname, newInfo,
1405                 existing.repliedServiceCount, existing.sentPacketCount, existing.ttl);
1406         mServices.put(serviceId, newService);
1407         return makeProbingInfo(serviceId, newService);
1408     }
1409 
1410     /**
1411      * Called when {@link MdnsAdvertiser} sent an advertisement for the given service.
1412      */
onAdvertisementSent(int serviceId, int sentPacketCount)1413     public void onAdvertisementSent(int serviceId, int sentPacketCount) {
1414         final ServiceRegistration registration = mServices.get(serviceId);
1415         if (registration == null) return;
1416 
1417         final long now = mDeps.elapsedRealTime();
1418         for (RecordInfo<?> record : registration.allRecords) {
1419             record.lastSentTimeMs = now;
1420             record.lastAdvertisedOnIpv4TimeMs = now;
1421             record.lastAdvertisedOnIpv6TimeMs = now;
1422         }
1423         registration.sentPacketCount += sentPacketCount;
1424     }
1425 
1426     /**
1427      * Called when {@link MdnsAdvertiser} sent a probing for the given service.
1428      */
onProbingSent(int serviceId, int sentPacketCount)1429     public void onProbingSent(int serviceId, int sentPacketCount) {
1430         final ServiceRegistration registration = mServices.get(serviceId);
1431         if (registration == null) return;
1432         registration.sentPacketCount += sentPacketCount;
1433     }
1434 
1435 
1436     /**
1437      * Compute:
1438      * 2001:db8::1 --> 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.B.D.0.1.0.0.2.ip6.arpa
1439      *
1440      * Or:
1441      * 192.0.2.123 --> 123.2.0.192.in-addr.arpa
1442      */
1443     @VisibleForTesting
getReverseDnsAddress(@onNull InetAddress addr)1444     public static String[] getReverseDnsAddress(@NonNull InetAddress addr) {
1445         // xxx.xxx.xxx.xxx.in-addr.arpa (up to 28 characters)
1446         // or 32 hex characters separated by dots + .ip6.arpa
1447         final byte[] addrBytes = addr.getAddress();
1448         final List<String> out = new ArrayList<>();
1449         if (addr instanceof Inet4Address) {
1450             for (int i = addrBytes.length - 1; i >= 0; i--) {
1451                 out.add(String.valueOf(Byte.toUnsignedInt(addrBytes[i])));
1452             }
1453             out.add("in-addr");
1454         } else {
1455             final String hexAddr = HexDump.toHexString(addrBytes);
1456 
1457             for (int i = hexAddr.length() - 1; i >= 0; i--) {
1458                 out.add(String.valueOf(hexAddr.charAt(i)));
1459             }
1460             out.add("ip6");
1461         }
1462         out.add("arpa");
1463 
1464         return out.toArray(new String[0]);
1465     }
1466 
splitFullyQualifiedName( @onNull NsdServiceInfo info, @NonNull String[] serviceType)1467     private static String[] splitFullyQualifiedName(
1468             @NonNull NsdServiceInfo info, @NonNull String[] serviceType) {
1469         final String[] split = new String[serviceType.length + 1];
1470         split[0] = info.getServiceName();
1471         System.arraycopy(serviceType, 0, split, 1, serviceType.length);
1472 
1473         return split;
1474     }
1475 
splitServiceType(@onNull NsdServiceInfo info)1476     private static String[] splitServiceType(@NonNull NsdServiceInfo info) {
1477         // String.split(pattern, 0) removes trailing empty strings, which would appear when
1478         // splitting "domain.name." (with a dot a the end), so this is what is needed here.
1479         final String[] split = info.getServiceType().split("\\.", 0);
1480         final String[] type = new String[split.length + 1];
1481         System.arraycopy(split, 0, type, 0, split.length);
1482         type[split.length] = LOCAL_TLD;
1483 
1484         return type;
1485     }
1486 
1487     /** Returns whether there will be an SRV record when registering the {@code info}. */
hasSrvRecord(@onNull NsdServiceInfo info)1488     private static boolean hasSrvRecord(@NonNull NsdServiceInfo info) {
1489         return info.getPort() > 0;
1490     }
1491 
1492     /** Returns whether there will be KEY record(s) when registering the {@code info}. */
hasKeyRecord(@onNull NsdServiceInfo info)1493     private static boolean hasKeyRecord(@NonNull NsdServiceInfo info) {
1494         return info.getPublicKey() != null;
1495     }
1496 
equals(@onNull MdnsRecord record, @Nullable RecordInfo<?> recordInfo)1497     private static boolean equals(@NonNull MdnsRecord record, @Nullable RecordInfo<?> recordInfo) {
1498         if (recordInfo == null) {
1499             return false;
1500         }
1501         return Objects.equals(record, recordInfo.record);
1502     }
1503 }
1504