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