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