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.util.ArrayMap; 23 import android.util.Pair; 24 25 import com.android.net.module.util.DnsUtils; 26 import com.android.server.connectivity.mdns.util.MdnsUtils; 27 28 import java.io.EOFException; 29 import java.util.ArrayList; 30 import java.util.Collection; 31 import java.util.List; 32 import java.util.Set; 33 34 /** A class that decodes mDNS responses from UDP packets. */ 35 public class MdnsResponseDecoder { 36 public static final int SUCCESS = 0; 37 private static final String TAG = "MdnsResponseDecoder"; 38 private final boolean allowMultipleSrvRecordsPerHost = 39 MdnsConfigs.allowMultipleSrvRecordsPerHost(); 40 @Nullable private final String[] serviceType; 41 private final MdnsUtils.Clock clock; 42 43 /** Constructs a new decoder that will extract responses for the given service type. */ MdnsResponseDecoder(@onNull MdnsUtils.Clock clock, @Nullable String[] serviceType)44 public MdnsResponseDecoder(@NonNull MdnsUtils.Clock clock, @Nullable String[] serviceType) { 45 this.clock = clock; 46 this.serviceType = serviceType; 47 } 48 findResponseWithPointer( List<MdnsResponse> responses, String[] pointer)49 private static MdnsResponse findResponseWithPointer( 50 List<MdnsResponse> responses, String[] pointer) { 51 if (responses != null) { 52 for (MdnsResponse response : responses) { 53 if (DnsUtils.equalsDnsLabelIgnoreDnsCase(response.getServiceName(), pointer)) { 54 return response; 55 } 56 } 57 } 58 return null; 59 } 60 findResponseWithHostName( List<MdnsResponse> responses, String[] hostName)61 private static MdnsResponse findResponseWithHostName( 62 List<MdnsResponse> responses, String[] hostName) { 63 if (responses != null) { 64 for (MdnsResponse response : responses) { 65 MdnsServiceRecord serviceRecord = response.getServiceRecord(); 66 if (serviceRecord == null) { 67 continue; 68 } 69 if (DnsUtils.equalsDnsLabelIgnoreDnsCase(serviceRecord.getServiceHost(), 70 hostName)) { 71 return response; 72 } 73 } 74 } 75 return null; 76 } 77 78 /** 79 * Decodes all mDNS responses for the desired service type from a packet. The class does not 80 * check the responses for completeness; the caller should do that. 81 * 82 * @param recvbuf The received data buffer to read from. 83 * @param length The length of received data buffer. 84 * @return A decoded {@link MdnsPacket}. 85 * @throws MdnsPacket.ParseException if a response packet could not be parsed. 86 */ 87 @NonNull parseResponse(@onNull byte[] recvbuf, int length, @NonNull MdnsFeatureFlags mdnsFeatureFlags)88 public static MdnsPacket parseResponse(@NonNull byte[] recvbuf, int length, 89 @NonNull MdnsFeatureFlags mdnsFeatureFlags) throws MdnsPacket.ParseException { 90 final MdnsPacketReader reader = new MdnsPacketReader(recvbuf, length, mdnsFeatureFlags); 91 92 final MdnsPacket mdnsPacket; 93 try { 94 final int transactionId = reader.readUInt16(); 95 int flags = reader.readUInt16(); 96 if ((flags & MdnsConstants.FLAGS_RESPONSE_MASK) != MdnsConstants.FLAGS_RESPONSE) { 97 throw new MdnsPacket.ParseException( 98 MdnsResponseErrorCode.ERROR_NOT_RESPONSE_MESSAGE, "Not a response", null); 99 } 100 101 mdnsPacket = MdnsPacket.parseRecordsSection(reader, flags, transactionId); 102 if (mdnsPacket.answers.size() < 1) { 103 throw new MdnsPacket.ParseException( 104 MdnsResponseErrorCode.ERROR_NO_ANSWERS, "Response has no answers", 105 null); 106 } 107 return mdnsPacket; 108 } catch (EOFException e) { 109 throw new MdnsPacket.ParseException(MdnsResponseErrorCode.ERROR_END_OF_FILE, 110 "Reached the end of the mDNS response unexpectedly.", e); 111 } 112 } 113 114 /** 115 * Augments a list of {@link MdnsResponse} with records from a packet. The class does not check 116 * the resulting responses for completeness; the caller should do that. 117 * 118 * @param mdnsPacket the response packet with the new records 119 * @param existingResponses list of existing responses. Will not be modified. 120 * @param interfaceIndex the network interface index (or 121 * {@link MdnsSocket#INTERFACE_INDEX_UNSPECIFIED} if not known) at which the packet was received 122 * @param network the network at which the packet was received, or null if it is unknown. 123 * @return The pair of 1) set of response instances that were modified or newly added. *not* 124 * including those which records were only updated with newer receive 125 * timestamps. 126 * 2) A copy of the original responses with some of them have records 127 * update or only contains receive time updated. 128 */ augmentResponses( @onNull MdnsPacket mdnsPacket, @NonNull Collection<MdnsResponse> existingResponses, int interfaceIndex, @Nullable Network network)129 public Pair<Set<MdnsResponse>, ArrayList<MdnsResponse>> augmentResponses( 130 @NonNull MdnsPacket mdnsPacket, 131 @NonNull Collection<MdnsResponse> existingResponses, int interfaceIndex, 132 @Nullable Network network) { 133 final ArrayList<MdnsRecord> records = new ArrayList<>( 134 mdnsPacket.questions.size() + mdnsPacket.answers.size() 135 + mdnsPacket.authorityRecords.size() + mdnsPacket.additionalRecords.size()); 136 records.addAll(mdnsPacket.answers); 137 records.addAll(mdnsPacket.authorityRecords); 138 records.addAll(mdnsPacket.additionalRecords); 139 140 final Set<MdnsResponse> modified = MdnsUtils.newSet(); 141 final ArrayList<MdnsResponse> responses = new ArrayList<>(existingResponses.size()); 142 final ArrayMap<MdnsResponse, MdnsResponse> augmentedToOriginal = new ArrayMap<>(); 143 for (MdnsResponse existing : existingResponses) { 144 final MdnsResponse copy = new MdnsResponse(existing); 145 responses.add(copy); 146 augmentedToOriginal.put(copy, existing); 147 } 148 // The response records are structured in a hierarchy, where some records reference 149 // others, as follows: 150 // 151 // PTR 152 // / \ 153 // / \ 154 // TXT SRV 155 // / \ 156 // / \ 157 // A AAAA 158 // 159 // But the order in which these records appear in the response packet is completely 160 // arbitrary. This means that we need to rescan the record list to construct each level of 161 // this hierarchy. 162 // 163 // PTR: service type -> service instance name 164 // 165 // SRV: service instance name -> host name (priority, weight) 166 // 167 // TXT: service instance name -> machine readable txt entries. 168 // 169 // A: host name -> IP address 170 171 // Loop 1: find PTR records, which identify distinct service instances. 172 long now = clock.elapsedRealtime(); 173 for (MdnsRecord record : records) { 174 if (record instanceof MdnsPointerRecord) { 175 String[] name = record.getName(); 176 if ((serviceType == null) || MdnsUtils.typeEqualsOrIsSubtype( 177 serviceType, name)) { 178 MdnsPointerRecord pointerRecord = (MdnsPointerRecord) record; 179 // Group PTR records that refer to the same service instance name into a single 180 // response. 181 MdnsResponse response = findResponseWithPointer(responses, 182 pointerRecord.getPointer()); 183 if (response == null) { 184 response = new MdnsResponse(now, pointerRecord.getPointer(), interfaceIndex, 185 network); 186 responses.add(response); 187 } 188 if (response.addPointerRecord((MdnsPointerRecord) record)) { 189 modified.add(response); 190 } 191 } 192 } 193 } 194 195 // Loop 2: find SRV and TXT records, which reference the pointer in the PTR record. 196 for (MdnsRecord record : records) { 197 if (record instanceof MdnsServiceRecord) { 198 MdnsServiceRecord serviceRecord = (MdnsServiceRecord) record; 199 MdnsResponse response = findResponseWithPointer(responses, serviceRecord.getName()); 200 if (response != null && response.setServiceRecord(serviceRecord)) { 201 response.dropUnmatchedAddressRecords(); 202 modified.add(response); 203 } 204 } else if (record instanceof MdnsTextRecord) { 205 MdnsTextRecord textRecord = (MdnsTextRecord) record; 206 MdnsResponse response = findResponseWithPointer(responses, textRecord.getName()); 207 if (response != null && response.setTextRecord(textRecord)) { 208 modified.add(response); 209 } 210 } 211 } 212 213 // Loop 3-1: find A and AAAA records and clear addresses if the cache-flush bit set, which 214 // reference the host name in the SRV record. 215 final List<MdnsInetAddressRecord> inetRecords = new ArrayList<>(); 216 for (MdnsRecord record : records) { 217 if (record instanceof MdnsInetAddressRecord) { 218 MdnsInetAddressRecord inetRecord = (MdnsInetAddressRecord) record; 219 inetRecords.add(inetRecord); 220 if (allowMultipleSrvRecordsPerHost) { 221 List<MdnsResponse> matchingResponses = 222 findResponsesWithHostName(responses, inetRecord.getName()); 223 for (MdnsResponse response : matchingResponses) { 224 // Per RFC6762 10.2, clear all address records if the cache-flush bit set. 225 // This bit, the cache-flush bit, tells neighboring hosts 226 // that this is not a shared record type. Instead of merging this new 227 // record additively into the cache in addition to any previous records with 228 // the same name, rrtype, and rrclass. 229 // TODO: All old records with that name, rrtype, and rrclass that were 230 // received more than one second ago are declared invalid, and marked 231 // to expire from the cache in one second. 232 if (inetRecord.getCacheFlush()) { 233 response.clearInet4AddressRecords(); 234 response.clearInet6AddressRecords(); 235 } 236 } 237 } else { 238 MdnsResponse response = 239 findResponseWithHostName(responses, inetRecord.getName()); 240 if (response != null) { 241 // Per RFC6762 10.2, clear all address records if the cache-flush bit set. 242 // This bit, the cache-flush bit, tells neighboring hosts 243 // that this is not a shared record type. Instead of merging this new 244 // record additively into the cache in addition to any previous records with 245 // the same name, rrtype, and rrclass. 246 // TODO: All old records with that name, rrtype, and rrclass that were 247 // received more than one second ago are declared invalid, and marked 248 // to expire from the cache in one second. 249 if (inetRecord.getCacheFlush()) { 250 response.clearInet4AddressRecords(); 251 response.clearInet6AddressRecords(); 252 } 253 } 254 } 255 } 256 } 257 258 // Loop 3-2: Assign addresses, which reference the host name in the SRV record. 259 for (MdnsInetAddressRecord inetRecord : inetRecords) { 260 if (allowMultipleSrvRecordsPerHost) { 261 List<MdnsResponse> matchingResponses = 262 findResponsesWithHostName(responses, inetRecord.getName()); 263 for (MdnsResponse response : matchingResponses) { 264 if (assignInetRecord(response, inetRecord)) { 265 final MdnsResponse originalResponse = augmentedToOriginal.get(response); 266 if (originalResponse == null 267 || !originalResponse.hasIdenticalRecord(inetRecord)) { 268 modified.add(response); 269 } 270 } 271 } 272 } else { 273 MdnsResponse response = 274 findResponseWithHostName(responses, inetRecord.getName()); 275 if (response != null) { 276 if (assignInetRecord(response, inetRecord)) { 277 final MdnsResponse originalResponse = augmentedToOriginal.get(response); 278 if (originalResponse == null 279 || !originalResponse.hasIdenticalRecord(inetRecord)) { 280 modified.add(response); 281 } 282 } 283 } 284 } 285 } 286 287 // Only responses that have new or modified address records were added to the modified set. 288 // Make sure responses that have lost address records are added to the set too. 289 for (int i = 0; i < augmentedToOriginal.size(); i++) { 290 final MdnsResponse augmented = augmentedToOriginal.keyAt(i); 291 final MdnsResponse original = augmentedToOriginal.valueAt(i); 292 if (augmented.getRecords().size() != original.getRecords().size()) { 293 modified.add(augmented); 294 } 295 } 296 297 return Pair.create(modified, responses); 298 } 299 assignInetRecord( MdnsResponse response, MdnsInetAddressRecord inetRecord)300 private static boolean assignInetRecord( 301 MdnsResponse response, MdnsInetAddressRecord inetRecord) { 302 if (inetRecord.getInet4Address() != null) { 303 return response.addInet4AddressRecord(inetRecord); 304 } else if (inetRecord.getInet6Address() != null) { 305 return response.addInet6AddressRecord(inetRecord); 306 } 307 return false; 308 } 309 findResponsesWithHostName( @ullable List<MdnsResponse> responses, String[] hostName)310 private static List<MdnsResponse> findResponsesWithHostName( 311 @Nullable List<MdnsResponse> responses, String[] hostName) { 312 if (responses == null || responses.isEmpty()) { 313 return List.of(); 314 } 315 316 List<MdnsResponse> result = null; 317 for (MdnsResponse response : responses) { 318 MdnsServiceRecord serviceRecord = response.getServiceRecord(); 319 if (serviceRecord == null) { 320 continue; 321 } 322 if (DnsUtils.equalsDnsLabelIgnoreDnsCase(serviceRecord.getServiceHost(), hostName)) { 323 if (result == null) { 324 result = new ArrayList<>(/* initialCapacity= */ responses.size()); 325 } 326 result.add(response); 327 } 328 } 329 return result == null ? List.of() : result; 330 } 331 }