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