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