1 /* 2 * Copyright (C) 2022 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.net.DnsResolver; 20 import android.text.TextUtils; 21 22 import com.android.net.module.util.CollectionUtils; 23 24 import java.io.IOException; 25 import java.util.ArrayList; 26 import java.util.Arrays; 27 import java.util.Objects; 28 29 /** 30 * A mDNS "NSEC" record, used in particular for negative responses (RFC6762 6.1). 31 */ 32 public class MdnsNsecRecord extends MdnsRecord { 33 private String[] mNextDomain; 34 private int[] mTypes; 35 MdnsNsecRecord(String[] name, MdnsPacketReader reader)36 public MdnsNsecRecord(String[] name, MdnsPacketReader reader) throws IOException { 37 this(name, reader, false); 38 } 39 MdnsNsecRecord(String[] name, MdnsPacketReader reader, boolean isQuestion)40 public MdnsNsecRecord(String[] name, MdnsPacketReader reader, boolean isQuestion) 41 throws IOException { 42 super(name, TYPE_NSEC, reader, isQuestion); 43 } 44 MdnsNsecRecord(String[] name, long receiptTimeMillis, boolean cacheFlush, long ttlMillis, String[] nextDomain, int[] types)45 public MdnsNsecRecord(String[] name, long receiptTimeMillis, boolean cacheFlush, long ttlMillis, 46 String[] nextDomain, int[] types) { 47 super(name, TYPE_NSEC, DnsResolver.CLASS_IN, receiptTimeMillis, cacheFlush, ttlMillis); 48 mNextDomain = nextDomain; 49 final int[] sortedTypes = Arrays.copyOf(types, types.length); 50 Arrays.sort(sortedTypes); 51 mTypes = sortedTypes; 52 } 53 getNextDomain()54 public String[] getNextDomain() { 55 return mNextDomain; 56 } 57 getTypes()58 public int[] getTypes() { 59 return mTypes; 60 } 61 62 @Override readData(MdnsPacketReader reader)63 protected void readData(MdnsPacketReader reader) throws IOException { 64 mNextDomain = reader.readLabels(); 65 mTypes = readTypes(reader); 66 } 67 readTypes(MdnsPacketReader reader)68 private int[] readTypes(MdnsPacketReader reader) throws IOException { 69 // See RFC3845 #2.1.2 70 final ArrayList<Integer> types = new ArrayList<>(); 71 int prevBlockNumber = -1; 72 while (reader.getRemaining() > 0) { 73 final int blockNumber = reader.readUInt8(); 74 if (blockNumber <= prevBlockNumber) { 75 throw new IOException( 76 "Unordered block number: " + blockNumber + " after " + prevBlockNumber); 77 } 78 prevBlockNumber = blockNumber; 79 final int bitmapLength = reader.readUInt8(); 80 if (bitmapLength > 32 || bitmapLength <= 0) { 81 throw new IOException("Invalid bitmap length: " + bitmapLength); 82 } 83 final byte[] bitmap = new byte[bitmapLength]; 84 reader.readBytes(bitmap); 85 86 for (int bitmapIndex = 0; bitmapIndex < bitmap.length; bitmapIndex++) { 87 final byte bitmapByte = bitmap[bitmapIndex]; 88 for (int bit = 0; bit < 8; bit++) { 89 if ((bitmapByte & (1 << (7 - bit))) != 0) { 90 types.add(blockNumber * 256 + bitmapIndex * 8 + bit); 91 } 92 } 93 } 94 } 95 96 return CollectionUtils.toIntArray(types); 97 } 98 99 @Override writeData(MdnsPacketWriter writer)100 protected void writeData(MdnsPacketWriter writer) throws IOException { 101 // Standard NSEC records should use no compression for the Next Domain Name field as per 102 // RFC3845 2.1.1, but for mDNS RFC6762 18.14 specifies that compression should be used. 103 writer.writeLabels(mNextDomain); 104 105 // type bitmaps: RFC3845 2.1.2 106 int typesBlockStart = 0; 107 int pendingBlockNumber = -1; 108 int blockLength = 0; 109 // Loop on types (which are sorted in increasing order) to find each block and determine 110 // their length; use writeTypeBlock once the length of each block has been found. 111 for (int i = 0; i < mTypes.length; i++) { 112 final int blockNumber = mTypes[i] / 256; 113 final int typeLowOrder = mTypes[i] % 256; 114 // If the low-order 8 bits are e.g. 0x10, bit number 16 (=0x10) will be set in the 115 // bitmap; this is the first bit of byte 2 (byte 0 is 0-7, 1 is 8-15, etc.) 116 final int byteIndex = typeLowOrder / 8; 117 118 if (pendingBlockNumber >= 0 && blockNumber != pendingBlockNumber) { 119 // Just reached a new block; write the previous one 120 writeTypeBlock(writer, typesBlockStart, i - 1, blockLength); 121 typesBlockStart = i; 122 blockLength = 0; 123 } 124 blockLength = Math.max(blockLength, byteIndex + 1); 125 pendingBlockNumber = blockNumber; 126 } 127 128 if (pendingBlockNumber >= 0) { 129 writeTypeBlock(writer, typesBlockStart, mTypes.length - 1, blockLength); 130 } 131 } 132 writeTypeBlock(MdnsPacketWriter writer, int typesStart, int typesEnd, int bytesInBlock)133 private void writeTypeBlock(MdnsPacketWriter writer, 134 int typesStart, int typesEnd, int bytesInBlock) throws IOException { 135 final int blockNumber = mTypes[typesStart] / 256; 136 final byte[] bytes = new byte[bytesInBlock]; 137 for (int i = typesStart; i <= typesEnd; i++) { 138 final int typeLowOrder = mTypes[i] % 256; 139 bytes[typeLowOrder / 8] |= 1 << (7 - (typeLowOrder % 8)); 140 } 141 writer.writeUInt8(blockNumber); 142 writer.writeUInt8(bytesInBlock); 143 writer.writeBytes(bytes); 144 } 145 146 @Override toString()147 public String toString() { 148 return "NSEC: NextDomain: " + TextUtils.join(".", mNextDomain) 149 + " Types " + Arrays.toString(mTypes); 150 } 151 152 @Override hashCode()153 public int hashCode() { 154 return Objects.hash(super.hashCode(), 155 Arrays.hashCode(mNextDomain), Arrays.hashCode(mTypes)); 156 } 157 158 @Override equals(Object other)159 public boolean equals(Object other) { 160 if (this == other) { 161 return true; 162 } 163 if (!(other instanceof MdnsNsecRecord)) { 164 return false; 165 } 166 167 return super.equals(other) 168 && Arrays.equals(mNextDomain, ((MdnsNsecRecord) other).mNextDomain) 169 && Arrays.equals(mTypes, ((MdnsNsecRecord) other).mTypes); 170 } 171 } 172