1 /* 2 * Copyright (C) 2023 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.util; 18 19 import static com.android.net.module.util.DnsUtils.equalsDnsLabelIgnoreDnsCase; 20 import static com.android.net.module.util.DnsUtils.equalsIgnoreDnsCase; 21 import static com.android.server.connectivity.mdns.MdnsConstants.FLAG_TRUNCATED; 22 23 import android.annotation.NonNull; 24 import android.annotation.Nullable; 25 import android.net.Network; 26 import android.os.Build; 27 import android.os.SystemClock; 28 import android.util.ArraySet; 29 import android.util.Pair; 30 31 import com.android.net.module.util.CollectionUtils; 32 import com.android.server.connectivity.mdns.MdnsConstants; 33 import com.android.server.connectivity.mdns.MdnsInetAddressRecord; 34 import com.android.server.connectivity.mdns.MdnsPacket; 35 import com.android.server.connectivity.mdns.MdnsPacketWriter; 36 import com.android.server.connectivity.mdns.MdnsRecord; 37 import com.android.server.connectivity.mdns.MdnsResponse; 38 import com.android.server.connectivity.mdns.MdnsServiceInfo; 39 40 import java.io.IOException; 41 import java.net.DatagramPacket; 42 import java.net.Inet4Address; 43 import java.net.Inet6Address; 44 import java.net.InetAddress; 45 import java.net.InetSocketAddress; 46 import java.nio.ByteBuffer; 47 import java.nio.CharBuffer; 48 import java.nio.charset.Charset; 49 import java.nio.charset.CharsetEncoder; 50 import java.nio.charset.StandardCharsets; 51 import java.time.Instant; 52 import java.util.ArrayList; 53 import java.util.Arrays; 54 import java.util.Collections; 55 import java.util.HashSet; 56 import java.util.List; 57 import java.util.Set; 58 59 /** 60 * Mdns utility functions. 61 */ 62 public class MdnsUtils { 63 MdnsUtils()64 private MdnsUtils() { } 65 66 /** 67 * Compare labels a equals b or a is suffix of b. 68 * 69 * @param a the type or subtype. 70 * @param b the base type 71 */ typeEqualsOrIsSubtype(@onNull String[] a, @NonNull String[] b)72 public static boolean typeEqualsOrIsSubtype(@NonNull String[] a, 73 @NonNull String[] b) { 74 return equalsDnsLabelIgnoreDnsCase(a, b) 75 || ((b.length == (a.length + 2)) 76 && equalsIgnoreDnsCase(b[1], MdnsConstants.SUBTYPE_LABEL) 77 && MdnsRecord.labelsAreSuffix(a, b)); 78 } 79 80 /** 81 * Create a ArraySet or HashSet based on the sdk version. 82 */ newSet()83 public static <Type> Set<Type> newSet() { 84 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 85 return new ArraySet<>(); 86 } else { 87 return new HashSet<>(); 88 } 89 } 90 91 92 /*** Check whether the target network matches the current network */ isNetworkMatched(@ullable Network targetNetwork, @Nullable Network currentNetwork)93 public static boolean isNetworkMatched(@Nullable Network targetNetwork, 94 @Nullable Network currentNetwork) { 95 return targetNetwork == null || targetNetwork.equals(currentNetwork); 96 } 97 98 /*** Check whether the target network matches any of the current networks */ isAnyNetworkMatched(@ullable Network targetNetwork, Set<Network> currentNetworks)99 public static boolean isAnyNetworkMatched(@Nullable Network targetNetwork, 100 Set<Network> currentNetworks) { 101 if (targetNetwork == null) { 102 return !currentNetworks.isEmpty(); 103 } 104 return currentNetworks.contains(targetNetwork); 105 } 106 107 /** 108 * Truncate a service name to up to maxLength UTF-8 bytes. 109 */ truncateServiceName(@onNull String originalName, int maxLength)110 public static String truncateServiceName(@NonNull String originalName, int maxLength) { 111 // UTF-8 is at most 4 bytes per character; return early in the common case where 112 // the name can't possibly be over the limit given its string length. 113 if (originalName.length() <= maxLength / 4) return originalName; 114 115 final Charset utf8 = StandardCharsets.UTF_8; 116 final CharsetEncoder encoder = utf8.newEncoder(); 117 final ByteBuffer out = ByteBuffer.allocate(maxLength); 118 // encode will write as many characters as possible to the out buffer, and just 119 // return an overflow code if there were too many characters (no need to check the 120 // return code here, this method truncates the name on purpose). 121 encoder.encode(CharBuffer.wrap(originalName), out, true /* endOfInput */); 122 return new String(out.array(), 0, out.position(), utf8); 123 } 124 125 /** 126 * Write the mdns packet from given MdnsPacket. 127 */ writeMdnsPacket(@onNull MdnsPacketWriter writer, @NonNull MdnsPacket packet)128 public static void writeMdnsPacket(@NonNull MdnsPacketWriter writer, @NonNull MdnsPacket packet) 129 throws IOException { 130 writer.writeUInt16(packet.transactionId); // Transaction ID (advertisement: 0) 131 writer.writeUInt16(packet.flags); // Response, authoritative (rfc6762 18.4) 132 writer.writeUInt16(packet.questions.size()); // questions count 133 writer.writeUInt16(packet.answers.size()); // answers count 134 writer.writeUInt16(packet.authorityRecords.size()); // authority entries count 135 writer.writeUInt16(packet.additionalRecords.size()); // additional records count 136 137 for (MdnsRecord record : packet.questions) { 138 // Questions do not have TTL or data 139 record.writeHeaderFields(writer); 140 } 141 for (MdnsRecord record : packet.answers) { 142 record.write(writer, 0L); 143 } 144 for (MdnsRecord record : packet.authorityRecords) { 145 record.write(writer, 0L); 146 } 147 for (MdnsRecord record : packet.additionalRecords) { 148 record.write(writer, 0L); 149 } 150 } 151 152 /** 153 * Create a raw DNS packet. 154 */ createRawDnsPacket(@onNull byte[] packetCreationBuffer, @NonNull MdnsPacket packet)155 public static byte[] createRawDnsPacket(@NonNull byte[] packetCreationBuffer, 156 @NonNull MdnsPacket packet) throws IOException { 157 // TODO: support packets over size (send in multiple packets with TC bit set) 158 final MdnsPacketWriter writer = new MdnsPacketWriter(packetCreationBuffer); 159 writeMdnsPacket(writer, packet); 160 161 final int len = writer.getWritePosition(); 162 return Arrays.copyOfRange(packetCreationBuffer, 0, len); 163 } 164 165 /** 166 * Writes the possible query content of an MdnsPacket into the data buffer. 167 * 168 * <p>This method is specifically for query packets. It writes the question and answer sections 169 * into the data buffer only. 170 * 171 * @param packetCreationBuffer The data buffer for the query content. 172 * @param packet The MdnsPacket to be written into the data buffer. 173 * @return A Pair containing: 174 * 1. The remaining MdnsPacket data that could not fit in the buffer. 175 * 2. The length of the data written to the buffer. 176 */ 177 @Nullable writePossibleMdnsPacket( @onNull byte[] packetCreationBuffer, @NonNull MdnsPacket packet)178 private static Pair<MdnsPacket, Integer> writePossibleMdnsPacket( 179 @NonNull byte[] packetCreationBuffer, @NonNull MdnsPacket packet) throws IOException { 180 MdnsPacket remainingPacket; 181 final MdnsPacketWriter writer = new MdnsPacketWriter(packetCreationBuffer); 182 writer.writeUInt16(packet.transactionId); // Transaction ID 183 184 final int flagsPos = writer.getWritePosition(); 185 writer.writeUInt16(0); // Flags, written later 186 writer.writeUInt16(0); // questions count, written later 187 writer.writeUInt16(0); // answers count, written later 188 writer.writeUInt16(0); // authority entries count, empty session for query 189 writer.writeUInt16(0); // additional records count, empty session for query 190 191 int writtenQuestions = 0; 192 int writtenAnswers = 0; 193 int lastValidPos = writer.getWritePosition(); 194 try { 195 for (MdnsRecord record : packet.questions) { 196 // Questions do not have TTL or data 197 record.writeHeaderFields(writer); 198 writtenQuestions++; 199 lastValidPos = writer.getWritePosition(); 200 } 201 for (MdnsRecord record : packet.answers) { 202 record.write(writer, 0L); 203 writtenAnswers++; 204 lastValidPos = writer.getWritePosition(); 205 } 206 remainingPacket = null; 207 } catch (IOException e) { 208 // Went over the packet limit; truncate 209 if (writtenQuestions == 0 && writtenAnswers == 0) { 210 // No space to write even one record: just throw (as subclass of IOException) 211 throw e; 212 } 213 214 // Set the last valid position as the final position (not as a rewind) 215 writer.rewind(lastValidPos); 216 writer.clearRewind(); 217 218 remainingPacket = new MdnsPacket(packet.flags, 219 packet.questions.subList( 220 writtenQuestions, packet.questions.size()), 221 packet.answers.subList( 222 writtenAnswers, packet.answers.size()), 223 Collections.emptyList(), /* authorityRecords */ 224 Collections.emptyList() /* additionalRecords */); 225 } 226 227 final int len = writer.getWritePosition(); 228 writer.rewind(flagsPos); 229 writer.writeUInt16(packet.flags | (remainingPacket == null ? 0 : FLAG_TRUNCATED)); 230 writer.writeUInt16(writtenQuestions); 231 writer.writeUInt16(writtenAnswers); 232 writer.unrewind(); 233 234 return Pair.create(remainingPacket, len); 235 } 236 237 /** 238 * Create Datagram packets from given MdnsPacket and InetSocketAddress. 239 * 240 * <p> If the MdnsPacket is too large for a single DatagramPacket, it will be split into 241 * multiple DatagramPackets. 242 */ createQueryDatagramPackets( @onNull byte[] packetCreationBuffer, @NonNull MdnsPacket packet, @NonNull InetSocketAddress destination)243 public static List<DatagramPacket> createQueryDatagramPackets( 244 @NonNull byte[] packetCreationBuffer, @NonNull MdnsPacket packet, 245 @NonNull InetSocketAddress destination) throws IOException { 246 final List<DatagramPacket> datagramPackets = new ArrayList<>(); 247 MdnsPacket remainingPacket = packet; 248 while (remainingPacket != null) { 249 final Pair<MdnsPacket, Integer> result = 250 writePossibleMdnsPacket(packetCreationBuffer, remainingPacket); 251 remainingPacket = result.first; 252 final int len = result.second; 253 final byte[] outBuffer = Arrays.copyOfRange(packetCreationBuffer, 0, len); 254 datagramPackets.add(new DatagramPacket(outBuffer, 0, outBuffer.length, destination)); 255 } 256 return datagramPackets; 257 } 258 259 /** 260 * Checks if the MdnsRecord needs to be renewed or not. 261 * 262 * <p>As per RFC6762 7.1 no need to query if remaining TTL is more than half the original one, 263 * so send the queries if half the TTL has passed. 264 */ isRecordRenewalNeeded(@onNull MdnsRecord mdnsRecord, final long now)265 public static boolean isRecordRenewalNeeded(@NonNull MdnsRecord mdnsRecord, final long now) { 266 return mdnsRecord.getTtl() > 0 267 && mdnsRecord.getRemainingTTL(now) <= mdnsRecord.getTtl() / 2; 268 } 269 270 /** 271 * Creates a new full subtype name with given service type and subtype labels. 272 * 273 * For example, given ["_http", "_tcp"] and "_printer", this method returns a new String array 274 * of ["_printer", "_sub", "_http", "_tcp"]. 275 */ constructFullSubtype(String[] serviceType, String subtype)276 public static String[] constructFullSubtype(String[] serviceType, String subtype) { 277 return CollectionUtils.prependArray(String.class, serviceType, subtype, 278 MdnsConstants.SUBTYPE_LABEL); 279 } 280 281 /** A wrapper class of {@link SystemClock} to be mocked in unit tests. */ 282 public static class Clock { 283 /** 284 * @see SystemClock#elapsedRealtime 285 */ elapsedRealtime()286 public long elapsedRealtime() { 287 return SystemClock.elapsedRealtime(); 288 } 289 } 290 291 /** 292 * Check all DatagramPackets with the same destination address. 293 */ checkAllPacketsWithSameAddress(List<DatagramPacket> packets)294 public static boolean checkAllPacketsWithSameAddress(List<DatagramPacket> packets) { 295 // No packet for address check 296 if (packets.isEmpty()) { 297 return true; 298 } 299 300 final InetAddress address = 301 ((InetSocketAddress) packets.get(0).getSocketAddress()).getAddress(); 302 for (DatagramPacket packet : packets) { 303 if (!address.equals(((InetSocketAddress) packet.getSocketAddress()).getAddress())) { 304 return false; 305 } 306 } 307 return true; 308 } 309 310 /** 311 * Build MdnsServiceInfo object from given MdnsResponse, service type labels and current time. 312 * 313 * @param response target service response 314 * @param serviceTypeLabels service type labels 315 * @param elapsedRealtimeMillis current time. 316 */ buildMdnsServiceInfoFromResponse(@onNull MdnsResponse response, @NonNull String[] serviceTypeLabels, long elapsedRealtimeMillis)317 public static MdnsServiceInfo buildMdnsServiceInfoFromResponse(@NonNull MdnsResponse response, 318 @NonNull String[] serviceTypeLabels, long elapsedRealtimeMillis) { 319 String[] hostName = null; 320 int port = 0; 321 if (response.hasServiceRecord()) { 322 hostName = response.getServiceRecord().getServiceHost(); 323 port = response.getServiceRecord().getServicePort(); 324 } 325 326 final List<String> ipv4Addresses = new ArrayList<>(); 327 final List<String> ipv6Addresses = new ArrayList<>(); 328 if (response.hasInet4AddressRecord()) { 329 for (MdnsInetAddressRecord inetAddressRecord : response.getInet4AddressRecords()) { 330 final Inet4Address inet4Address = inetAddressRecord.getInet4Address(); 331 ipv4Addresses.add((inet4Address == null) ? null : inet4Address.getHostAddress()); 332 } 333 } 334 if (response.hasInet6AddressRecord()) { 335 for (MdnsInetAddressRecord inetAddressRecord : response.getInet6AddressRecords()) { 336 final Inet6Address inet6Address = inetAddressRecord.getInet6Address(); 337 ipv6Addresses.add((inet6Address == null) ? null : inet6Address.getHostAddress()); 338 } 339 } 340 String serviceInstanceName = response.getServiceInstanceName(); 341 if (serviceInstanceName == null) { 342 throw new IllegalStateException( 343 "mDNS response must have non-null service instance name"); 344 } 345 List<String> textStrings = null; 346 List<MdnsServiceInfo.TextEntry> textEntries = null; 347 if (response.hasTextRecord()) { 348 textStrings = response.getTextRecord().getStrings(); 349 textEntries = response.getTextRecord().getEntries(); 350 } 351 Instant now = Instant.now(); 352 // TODO: Throw an error message if response doesn't have Inet6 or Inet4 address. 353 return new MdnsServiceInfo( 354 serviceInstanceName, 355 serviceTypeLabels, 356 response.getSubtypes(), 357 hostName, 358 port, 359 ipv4Addresses, 360 ipv6Addresses, 361 textStrings, 362 textEntries, 363 response.getInterfaceIndex(), 364 response.getNetwork(), 365 now.plusMillis(response.getMinRemainingTtl(elapsedRealtimeMillis))); 366 } 367 }