• 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.tv.mdnsoffloadmanager;
18 
19 import android.util.Log;
20 
21 import androidx.annotation.NonNull;
22 
23 import java.nio.charset.StandardCharsets;
24 import java.util.ArrayList;
25 import java.util.List;
26 import java.util.Objects;
27 
28 import device.google.atv.mdns_offload.IMdnsOffload.MdnsProtocolData.MatchCriteria;
29 
30 /**
31  * Tool class to help read mdns data from a fully formed mDNS response packet.
32  */
33 public final class MdnsPacketParser {
34     private static final String TAG = MdnsPacketParser.class.getSimpleName();
35 
36     private static final int OFFSET_QUERIES_COUNT = 4;
37     private static final int OFFSET_ANSWERS_COUNT = 6;
38     private static final int OFFSET_AUTHORITY_COUNT = 8;
39     private static final int OFFSET_ADDITIONAL_COUNT = 10;
40     private static final int OFFSET_DATA_SECTION_START = 12;
41 
42     private final byte[] mMdnsData;
43     private int mCursorIndex;
44 
MdnsPacketParser(@onNull byte[] mDNSData)45     private MdnsPacketParser(@NonNull byte[] mDNSData) {
46         this.mMdnsData = mDNSData;
47     }
48 
49     /**
50      * Extracts a label starting at offset and then follows RFC1035-4.1.4 The offset should start
51      * either at a data length value or at a pointer value.
52      */
extractFullName(@onNull byte[] array, int offset)53     public static String extractFullName(@NonNull byte[] array, int offset) {
54         MdnsPacketParser parser = new MdnsPacketParser(array);
55         parser.setCursor(offset);
56         StringBuilder builder = new StringBuilder();
57 
58         while (!parser.isCursorOnRootLabel()) {
59             if (parser.isCursorOnPointer()) {
60                 parser.setCursor(parser.pollPointerOffset());
61             } else if (parser.isCursorOnLabel()) {
62                 builder.append(parser.pollLabel());
63                 builder.append('.');
64             } else {
65                 throw new IllegalArgumentException("mDNS response packet is badly formed.");
66             }
67         }
68         return builder.toString();
69     }
70 
71     /**
72      * Finds all the RRNAMEs and RRTYPEs in the mdns response packet provided. Expects a packet only
73      * with responses.
74      */
extractMatchCriteria(@onNull byte[] mdnsResponsePacket)75     public static List<MatchCriteria> extractMatchCriteria(@NonNull byte[] mdnsResponsePacket) {
76         Objects.requireNonNull(mdnsResponsePacket);
77 
78         // Parse MdnsPacket and read labels and find
79         List<MatchCriteria> criteriaList = new ArrayList<>();
80         MdnsPacketParser parser = new MdnsPacketParser(mdnsResponsePacket);
81 
82         if (parser.getQueriesCount() != 0
83                 || parser.getAuthorityCount() != 0
84                 || parser.getAdditionalCount() != 0) {
85             throw new IllegalArgumentException(
86                     "mDNS response packet contains data that is not answers");
87         }
88         int answersToRead = parser.getAnswersCount();
89 
90         parser.moveToDataSection();
91         while (answersToRead > 0) {
92             // Each record starts with the RRNAME, so the offset is correct for the criteria.
93             MatchCriteria criteria = new MatchCriteria();
94             criteria.nameOffset = parser.getCursorOffset();
95 
96             /// Skip labels first
97             while (parser.isCursorOnLabel()) {
98                 parser.pollLabel();
99             }
100             // We can be on a root label or on a pointer. Skip both.
101             if (parser.isCursorOnRootLabel()) {
102                 parser.skipBytes(1);
103             } else if (parser.isCursorOnPointer()) {
104                 parser.pollPointerOffset();
105             }
106 
107             // The cursor must be on the RRTYPE.
108             criteria.type = parser.pollUint16();
109 
110             // The next 6 bytes point to cache flush, rrclass, and ttl
111             parser.skipBytes(6);
112 
113             // Now the index points to the data length on 2 bytes
114             int dataLength = parser.pollUint16();
115 
116             // Then we can skip those data bytes.
117             parser.skipBytes(dataLength);
118 
119             // Criteria is complete, it can be added.
120             // https://b.corp.google.com/issues/323169340#comment5
121             if (criteria.type != 28) {
122                 criteriaList.add(criteria);
123 
124                 if (Log.isLoggable(TAG, Log.DEBUG)) {
125                   String name = extractFullName(mdnsResponsePacket, criteria.nameOffset);
126                   String type = Integer.toString(criteria.type);
127                   switch(criteria.type) {
128                     case 1:
129                       type = "A";
130                       break;
131                     case 28:
132                       type = "AAAA";
133                       break;
134                     case 12:
135                       type = "PTR";
136                       break;
137                     case 33:
138                       type = "SRV";
139                       break;
140                     case 16:
141                       type = "TXT";
142                       break;
143                     default:
144                       break;
145                   }
146                   Log.d(TAG, "Adding match criteria: %s type %s".formatted(name, type));
147                 }
148             }
149 
150             answersToRead--;
151         }
152         if (parser.hasContent()) {
153             // The packet is badly formed. All answers where read successfully, but data remains
154             // available.
155             throw new IllegalArgumentException(
156                     "mDNS response packet is badly formed. Too much data.");
157         }
158 
159         return criteriaList;
160     }
161 
hasContent()162     private boolean hasContent() {
163         return mCursorIndex < mMdnsData.length;
164     }
165 
setCursor(int position)166     private void setCursor(int position) {
167         if (position < 0) {
168             throw new IllegalArgumentException("Setting cursor on negative offset is not allowed.");
169         }
170         mCursorIndex = position;
171     }
172 
getCursorOffset()173     private int getCursorOffset() {
174         return mCursorIndex;
175     }
176 
skipBytes(int bytesToSkip)177     private void skipBytes(int bytesToSkip) {
178         mCursorIndex += bytesToSkip;
179     }
180 
getQueriesCount()181     private int getQueriesCount() {
182         return readUint16(OFFSET_QUERIES_COUNT);
183     }
184 
getAnswersCount()185     private int getAnswersCount() {
186         return readUint16(OFFSET_ANSWERS_COUNT);
187     }
188 
getAuthorityCount()189     private int getAuthorityCount() {
190         return readUint16(OFFSET_AUTHORITY_COUNT);
191     }
192 
getAdditionalCount()193     private int getAdditionalCount() {
194         return readUint16(OFFSET_ADDITIONAL_COUNT);
195     }
196 
moveToDataSection()197     private void moveToDataSection() {
198         mCursorIndex = OFFSET_DATA_SECTION_START;
199     }
200 
pollLabel()201     private String pollLabel() {
202         int labelSize = readUint8(mCursorIndex);
203         mCursorIndex++;
204         if (mCursorIndex + labelSize > mMdnsData.length) {
205             throw new IllegalArgumentException(
206                     "mDNS response packet is badly formed. Not enough data.");
207         }
208         String value = new String(mMdnsData, mCursorIndex, labelSize, StandardCharsets.UTF_8);
209         mCursorIndex += labelSize;
210         return value;
211     }
212 
isCursorOnLabel()213     private boolean isCursorOnLabel() {
214         return !isCursorOnRootLabel() && (readUint8(mCursorIndex) & 0b11000000) == 0b00000000;
215     }
216 
isCursorOnPointer()217     private boolean isCursorOnPointer() {
218         return (readUint8(mCursorIndex) & 0b11000000) == 0b11000000;
219     }
220 
isCursorOnRootLabel()221     private boolean isCursorOnRootLabel() {
222         return readUint8(mCursorIndex) == 0;
223     }
224 
pollPointerOffset()225     private int pollPointerOffset() {
226         int value = readUint16(mCursorIndex) & 0b0011111111111111;
227         mCursorIndex += 2;
228         return value;
229     }
230 
readUint8(int offset)231     private int readUint8(int offset) {
232         if (offset >= mMdnsData.length) {
233             throw new IllegalArgumentException(
234                     "mDNS response packet is badly formed. Not enough data.");
235         }
236         return ((int) mMdnsData[offset]) & 0xff;
237     }
238 
readUint16(int offset)239     private int readUint16(int offset) {
240         if (offset + 1 >= mMdnsData.length) {
241             throw new IllegalArgumentException(
242                     "mDNS response packet is badly formed. Not enough data.");
243         }
244         return (readUint8(offset) << 8) + readUint8(offset + 1);
245     }
246 
pollUint16()247     private int pollUint16() {
248         int value = readUint16(mCursorIndex);
249         mCursorIndex += 2;
250         return value;
251     }
252 }
253