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