1 /* 2 * Copyright (C) 2021 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 android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.net.Network; 22 import android.os.SystemClock; 23 import android.text.TextUtils; 24 25 import com.android.internal.annotations.VisibleForTesting; 26 import com.android.net.module.util.DnsUtils; 27 28 import java.io.IOException; 29 import java.time.Instant; 30 import java.util.ArrayList; 31 import java.util.Collections; 32 import java.util.Iterator; 33 import java.util.LinkedList; 34 import java.util.List; 35 import java.util.Objects; 36 37 /** An mDNS response. */ 38 public class MdnsResponse { 39 public static final long EXPIRATION_NEVER = Long.MAX_VALUE; 40 private final List<MdnsRecord> records; 41 private final List<MdnsPointerRecord> pointerRecords; 42 private MdnsServiceRecord serviceRecord; 43 private MdnsTextRecord textRecord; 44 @NonNull private List<MdnsInetAddressRecord> inet4AddressRecords; 45 @NonNull private List<MdnsInetAddressRecord> inet6AddressRecords; 46 private long lastUpdateTime; 47 private final int interfaceIndex; 48 @Nullable private final Network network; 49 @NonNull private final String[] serviceName; 50 51 /** Constructs a new, empty response. */ MdnsResponse(long now, @NonNull String[] serviceName, int interfaceIndex, @Nullable Network network)52 public MdnsResponse(long now, @NonNull String[] serviceName, int interfaceIndex, 53 @Nullable Network network) { 54 lastUpdateTime = now; 55 records = new LinkedList<>(); 56 pointerRecords = new LinkedList<>(); 57 inet4AddressRecords = new ArrayList<>(); 58 inet6AddressRecords = new ArrayList<>(); 59 this.interfaceIndex = interfaceIndex; 60 this.network = network; 61 this.serviceName = serviceName; 62 } 63 MdnsResponse(@onNull MdnsResponse base)64 public MdnsResponse(@NonNull MdnsResponse base) { 65 records = new ArrayList<>(base.records); 66 pointerRecords = new ArrayList<>(base.pointerRecords); 67 serviceRecord = base.serviceRecord; 68 textRecord = base.textRecord; 69 inet4AddressRecords = new ArrayList<>(base.inet4AddressRecords); 70 inet6AddressRecords = new ArrayList<>(base.inet6AddressRecords); 71 lastUpdateTime = base.lastUpdateTime; 72 serviceName = base.serviceName; 73 interfaceIndex = base.interfaceIndex; 74 network = base.network; 75 } 76 77 /** 78 * Compare records for equality, including their TTL. 79 * 80 * MdnsRecord#equals ignores TTL and receiptTimeMillis, but methods in this class need to update 81 * records when the TTL changes (especially for goodbye announcements). 82 */ recordsAreSame(MdnsRecord a, MdnsRecord b)83 private boolean recordsAreSame(MdnsRecord a, MdnsRecord b) { 84 if (!Objects.equals(a, b)) return false; 85 return a == null || a.getTtl() == b.getTtl(); 86 } 87 addOrReplaceRecord(@onNull T record, @NonNull List<T> recordsList)88 private <T extends MdnsRecord> boolean addOrReplaceRecord(@NonNull T record, 89 @NonNull List<T> recordsList) { 90 final int existing = recordsList.indexOf(record); 91 boolean isSame = false; 92 if (existing >= 0) { 93 isSame = recordsAreSame(record, recordsList.get(existing)); 94 final MdnsRecord existedRecord = recordsList.remove(existing); 95 records.remove(existedRecord); 96 } 97 recordsList.add(record); 98 records.add(record); 99 return !isSame; 100 } 101 102 /** 103 * @return True if this response contains an identical (original TTL included) record. 104 */ hasIdenticalRecord(@onNull MdnsRecord record)105 public boolean hasIdenticalRecord(@NonNull MdnsRecord record) { 106 final int existing = records.indexOf(record); 107 return existing >= 0 && recordsAreSame(record, records.get(existing)); 108 } 109 110 /** 111 * Adds a pointer record. 112 * 113 * @return <code>true</code> if the record was added, or <code>false</code> if a matching 114 * pointer record is already present in the response with the same TTL. 115 */ addPointerRecord(MdnsPointerRecord pointerRecord)116 public synchronized boolean addPointerRecord(MdnsPointerRecord pointerRecord) { 117 if (!DnsUtils.equalsDnsLabelIgnoreDnsCase(serviceName, pointerRecord.getPointer())) { 118 throw new IllegalArgumentException( 119 "Pointer records for different service names cannot be added"); 120 } 121 return addOrReplaceRecord(pointerRecord, pointerRecords); 122 } 123 124 /** Gets the pointer records. */ getPointerRecords()125 public synchronized List<MdnsPointerRecord> getPointerRecords() { 126 // Returns a shallow copy. 127 return new LinkedList<>(pointerRecords); 128 } 129 hasPointerRecords()130 public synchronized boolean hasPointerRecords() { 131 return !pointerRecords.isEmpty(); 132 } 133 134 @VisibleForTesting clearPointerRecords()135 synchronized void clearPointerRecords() { 136 pointerRecords.clear(); 137 } 138 hasSubtypes()139 public synchronized boolean hasSubtypes() { 140 for (MdnsPointerRecord pointerRecord : pointerRecords) { 141 if (pointerRecord.hasSubtype()) { 142 return true; 143 } 144 } 145 return false; 146 } 147 148 @Nullable getSubtypes()149 public synchronized List<String> getSubtypes() { 150 List<String> subtypes = null; 151 for (MdnsPointerRecord pointerRecord : pointerRecords) { 152 String pointerRecordSubtype = pointerRecord.getSubtype(); 153 if (pointerRecordSubtype != null) { 154 if (subtypes == null) { 155 subtypes = new LinkedList<>(); 156 } 157 subtypes.add(pointerRecordSubtype); 158 } 159 } 160 161 return subtypes; 162 } 163 164 @VisibleForTesting removeSubtypes()165 public synchronized void removeSubtypes() { 166 Iterator<MdnsPointerRecord> iter = pointerRecords.iterator(); 167 while (iter.hasNext()) { 168 MdnsPointerRecord pointerRecord = iter.next(); 169 if (pointerRecord.hasSubtype()) { 170 iter.remove(); 171 } 172 } 173 } 174 175 /** Sets the service record. */ setServiceRecord(MdnsServiceRecord serviceRecord)176 public synchronized boolean setServiceRecord(MdnsServiceRecord serviceRecord) { 177 boolean isSame = recordsAreSame(this.serviceRecord, serviceRecord); 178 if (this.serviceRecord != null) { 179 records.remove(this.serviceRecord); 180 } 181 this.serviceRecord = serviceRecord; 182 if (this.serviceRecord != null) { 183 records.add(this.serviceRecord); 184 } 185 return !isSame; 186 } 187 188 /** Gets the service record. */ getServiceRecord()189 public synchronized MdnsServiceRecord getServiceRecord() { 190 return serviceRecord; 191 } 192 hasServiceRecord()193 public synchronized boolean hasServiceRecord() { 194 return serviceRecord != null; 195 } 196 197 /** Sets the text record. */ setTextRecord(MdnsTextRecord textRecord)198 public synchronized boolean setTextRecord(MdnsTextRecord textRecord) { 199 boolean isSame = recordsAreSame(this.textRecord, textRecord); 200 if (this.textRecord != null) { 201 records.remove(this.textRecord); 202 } 203 this.textRecord = textRecord; 204 if (this.textRecord != null) { 205 records.add(this.textRecord); 206 } 207 return !isSame; 208 } 209 210 /** Gets the text record. */ getTextRecord()211 public synchronized MdnsTextRecord getTextRecord() { 212 return textRecord; 213 } 214 hasTextRecord()215 public synchronized boolean hasTextRecord() { 216 return textRecord != null; 217 } 218 219 /** Add the IPv4 address record. */ addInet4AddressRecord( @onNull MdnsInetAddressRecord newInet4AddressRecord)220 public synchronized boolean addInet4AddressRecord( 221 @NonNull MdnsInetAddressRecord newInet4AddressRecord) { 222 return addOrReplaceRecord(newInet4AddressRecord, inet4AddressRecords); 223 } 224 225 /** Gets the IPv4 address records. */ 226 @NonNull getInet4AddressRecords()227 public synchronized List<MdnsInetAddressRecord> getInet4AddressRecords() { 228 return Collections.unmodifiableList(inet4AddressRecords); 229 } 230 231 /** Return the first IPv4 address record or null if no record. */ 232 @Nullable getInet4AddressRecord()233 public synchronized MdnsInetAddressRecord getInet4AddressRecord() { 234 return inet4AddressRecords.isEmpty() ? null : inet4AddressRecords.get(0); 235 } 236 237 /** Check whether response has IPv4 address record */ hasInet4AddressRecord()238 public synchronized boolean hasInet4AddressRecord() { 239 return !inet4AddressRecords.isEmpty(); 240 } 241 242 /** Clear all IPv4 address records */ clearInet4AddressRecords()243 synchronized void clearInet4AddressRecords() { 244 for (MdnsInetAddressRecord record : inet4AddressRecords) { 245 records.remove(record); 246 } 247 inet4AddressRecords.clear(); 248 } 249 250 /** Sets the IPv6 address records. */ addInet6AddressRecord( @onNull MdnsInetAddressRecord newInet6AddressRecord)251 public synchronized boolean addInet6AddressRecord( 252 @NonNull MdnsInetAddressRecord newInet6AddressRecord) { 253 return addOrReplaceRecord(newInet6AddressRecord, inet6AddressRecords); 254 } 255 256 /** 257 * Returns the index of the network interface at which this response was received. Can be set to 258 * {@link MdnsSocket#INTERFACE_INDEX_UNSPECIFIED} if unset. 259 */ getInterfaceIndex()260 public int getInterfaceIndex() { 261 return interfaceIndex; 262 } 263 264 /** 265 * Returns the network at which this response was received, or null if the network is unknown. 266 */ 267 @Nullable getNetwork()268 public Network getNetwork() { 269 return network; 270 } 271 272 /** Gets all IPv6 address records. */ getInet6AddressRecords()273 public synchronized List<MdnsInetAddressRecord> getInet6AddressRecords() { 274 return Collections.unmodifiableList(inet6AddressRecords); 275 } 276 277 /** Return the first IPv6 address record or null if no record. */ 278 @Nullable getInet6AddressRecord()279 public synchronized MdnsInetAddressRecord getInet6AddressRecord() { 280 return inet6AddressRecords.isEmpty() ? null : inet6AddressRecords.get(0); 281 } 282 283 /** Check whether response has IPv6 address record */ hasInet6AddressRecord()284 public synchronized boolean hasInet6AddressRecord() { 285 return !inet6AddressRecords.isEmpty(); 286 } 287 288 /** Clear all IPv6 address records */ clearInet6AddressRecords()289 synchronized void clearInet6AddressRecords() { 290 for (MdnsInetAddressRecord record : inet6AddressRecords) { 291 records.remove(record); 292 } 293 inet6AddressRecords.clear(); 294 } 295 296 /** Gets all of the records. */ getRecords()297 public synchronized List<MdnsRecord> getRecords() { 298 return new LinkedList<>(records); 299 } 300 301 /** 302 * Drop address records if they are for a hostname that does not match the service record. 303 * 304 * @return True if the records were dropped. 305 */ dropUnmatchedAddressRecords()306 public synchronized boolean dropUnmatchedAddressRecords() { 307 if (this.serviceRecord == null) return false; 308 boolean dropAddressRecords = false; 309 310 for (MdnsInetAddressRecord inetAddressRecord : getInet4AddressRecords()) { 311 if (!DnsUtils.equalsDnsLabelIgnoreDnsCase( 312 this.serviceRecord.getServiceHost(), inetAddressRecord.getName())) { 313 dropAddressRecords = true; 314 } 315 } 316 for (MdnsInetAddressRecord inetAddressRecord : getInet6AddressRecords()) { 317 if (!DnsUtils.equalsDnsLabelIgnoreDnsCase( 318 this.serviceRecord.getServiceHost(), inetAddressRecord.getName())) { 319 dropAddressRecords = true; 320 } 321 } 322 323 if (dropAddressRecords) { 324 clearInet4AddressRecords(); 325 clearInet6AddressRecords(); 326 return true; 327 } 328 return false; 329 } 330 331 /** 332 * Tests if the response is complete. A response is considered complete if it contains SRV, 333 * TXT, and A (for IPv4) or AAAA (for IPv6) records. The service type->name mapping is always 334 * known when constructing a MdnsResponse, so this may return true when there is no PTR record. 335 */ isComplete()336 public synchronized boolean isComplete() { 337 return (serviceRecord != null) 338 && (textRecord != null) 339 && (!inet4AddressRecords.isEmpty() || !inet6AddressRecords.isEmpty()); 340 } 341 342 /** 343 * Returns the key for this response. The key uniquely identifies the response by its service 344 * name. 345 */ 346 @Nullable getServiceInstanceName()347 public String getServiceInstanceName() { 348 return serviceName.length > 0 ? serviceName[0] : null; 349 } 350 351 @NonNull getServiceName()352 public String[] getServiceName() { 353 return serviceName; 354 } 355 356 /** Get the min remaining ttl time from received records */ getMinRemainingTtl(long now)357 public long getMinRemainingTtl(long now) { 358 long minRemainingTtl = EXPIRATION_NEVER; 359 // TODO: Check other records(A, AAAA, TXT) ttl time. 360 if (!hasServiceRecord()) { 361 return EXPIRATION_NEVER; 362 } 363 // Check ttl time. 364 long remainingTtl = serviceRecord.getRemainingTTL(now); 365 if (remainingTtl < minRemainingTtl) { 366 minRemainingTtl = remainingTtl; 367 } 368 return minRemainingTtl; 369 } 370 371 /** 372 * Tests if this response is a goodbye message. This will be true if a service record is present 373 * and any of the records have a TTL of 0. 374 */ isGoodbye()375 public synchronized boolean isGoodbye() { 376 if (getServiceInstanceName() != null) { 377 for (MdnsRecord record : records) { 378 // Expiring PTR records with subtypes just signal a change in known supported 379 // criteria, not the device itself going offline, so ignore those. 380 if ((record instanceof MdnsPointerRecord) 381 && ((MdnsPointerRecord) record).hasSubtype()) { 382 continue; 383 } 384 385 if (record.getTtl() == 0) { 386 return true; 387 } 388 } 389 } 390 return false; 391 } 392 393 /** 394 * Writes the response to a packet. 395 * 396 * @param writer The writer to use. 397 * @param now The current time. This is used to write updated TTLs that reflect the remaining 398 * TTL 399 * since the response was received. 400 * @return The number of records that were written. 401 * @throws IOException If an error occurred while writing (typically indicating overflow). 402 */ write(MdnsPacketWriter writer, long now)403 public synchronized int write(MdnsPacketWriter writer, long now) throws IOException { 404 int count = 0; 405 for (MdnsPointerRecord pointerRecord : pointerRecords) { 406 pointerRecord.write(writer, now); 407 ++count; 408 } 409 410 if (serviceRecord != null) { 411 serviceRecord.write(writer, now); 412 ++count; 413 } 414 415 if (textRecord != null) { 416 textRecord.write(writer, now); 417 ++count; 418 } 419 420 for (MdnsInetAddressRecord inetAddressRecord : inet4AddressRecords) { 421 inetAddressRecord.write(writer, now); 422 ++count; 423 } 424 425 for (MdnsInetAddressRecord inetAddressRecord : inet6AddressRecords) { 426 inetAddressRecord.write(writer, now); 427 ++count; 428 } 429 430 return count; 431 } 432 433 @Override toString()434 public String toString() { 435 return "Name: " + TextUtils.join(".", serviceName) 436 + ", pointerRecords: " + pointerRecords 437 + ", serviceRecord: " + serviceRecord 438 + ", textRecord: " + textRecord 439 + ", inet4AddressRecords: " + inet4AddressRecords 440 + ", inet6AddressRecords: " + inet6AddressRecords 441 + ", interfaceIndex: " + interfaceIndex 442 + ", network: " + network 443 + ", lastUpdateTime: " + Instant.now().minusMillis( 444 SystemClock.elapsedRealtime() - lastUpdateTime); 445 } 446 } 447