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