• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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