Add DomainsUtils class to encode/decode domain names.

Both encode and deocde methods support message compression which is
described in RFC1035 section 4.1.4. We can leverage both methods in
a follow-up CL to parse DHCPv4 option 119 and DNSSL RA option.

This cl also adds maxLabelCount argument for DnsRecordParser#parseName
to prevent the potential recursion from overflowing the stack.

Bug: 189865016
Test: atest NetworkStaticLibTests
Change-Id: I624649c224f141848a475ac85c56012d138c5224
This commit is contained in:
fqjc86 2023-06-08 19:46:45 +08:00 • committed by Xiao Ma
commit cece401526
4 changed files with 365 additions and 4 deletions

View file

@ -35,6 +35,7 @@ java_library {
name: "net-utils-device-common",
srcs: [
"device/com/android/net/module/util/DeviceConfigUtils.java",
"device/com/android/net/module/util/DomainUtils.java",
"device/com/android/net/module/util/FdEventsReader.java",
"device/com/android/net/module/util/NetworkMonitorUtils.java",
"device/com/android/net/module/util/PacketReader.java",

View file

@ -0,0 +1,143 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.net.module.util;
import android.util.ArrayMap;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.net.module.util.DnsPacketUtils.DnsRecordParser;
import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
/**
* Utilities for encoding/decoding the domain name or domain search list.
*
* @hide
*/
public final class DomainUtils {
private static final String TAG = "DomainUtils";
private static final int MAX_OPTION_LEN = 255;
@NonNull
private static String getSubstring(@NonNull final String string, @NonNull final String[] labels,
int index) {
int beginIndex = 0;
for (int i = 0; i < index; i++) {
beginIndex += labels[i].length() + 1; // include the dot
}
return string.substring(beginIndex);
}
/**
* Encode the given single domain name to byte array, comply with RFC1035 section-3.1.
*
* @return null if the given domain string is invalid, otherwise, return a byte array
* wrapping the encoded domain, not including any padded octets, caller should
* pad zero octets at the end if needed.
*/
@Nullable
public static byte[] encode(@NonNull final String domain) {
if (!DnsRecordParser.isHostName(domain)) return null;
return encode(new String[]{ domain }, false /* compression */);
}
/**
* Encode the given multiple domain names to byte array, comply with RFC1035 section-3.1
* and section 4.1.4 (message compression) if enabled.
*
* @return Null if encode fails due to BufferOverflowException, otherwise, return a byte
* array wrapping the encoded domains, not including any padded octets, caller
* should pad zero octets at the end if needed. The byte array may be empty if
* the given domain strings are invalid.
*/
@Nullable
public static byte[] encode(@NonNull final String[] domains, boolean compression) {
try {
final ByteBuffer buffer = ByteBuffer.allocate(MAX_OPTION_LEN);
final ArrayMap<String, Integer> offsetMap = new ArrayMap<>();
for (int i = 0; i < domains.length; i++) {
if (!DnsRecordParser.isHostName(domains[i])) {
Log.e(TAG, "Skip invalid domain name " + domains[i]);
continue;
}
final String[] labels = domains[i].split("\\.");
for (int j = 0; j < labels.length; j++) {
if (compression) {
final String suffix = getSubstring(domains[i], labels, j);
if (offsetMap.containsKey(suffix)) {
int offsetOfSuffix = offsetMap.get(suffix);
offsetOfSuffix |= 0xC000;
buffer.putShort((short) offsetOfSuffix);
break; // unnecessary to put the compressed string into map
} else {
offsetMap.put(suffix, buffer.position());
}
}
// encode the domain name string without compression when:
// - compression feature isn't enabled,
// - suffix does not match any string in the map.
final byte[] labelBytes = labels[j].getBytes(StandardCharsets.UTF_8);
buffer.put((byte) labelBytes.length);
buffer.put(labelBytes);
if (j == labels.length - 1) {
// Pad terminate label at the end of last label.
buffer.put((byte) 0);
}
}
}
buffer.flip();
final byte[] out = new byte[buffer.limit()];
buffer.get(out);
return out;
} catch (BufferOverflowException e) {
Log.e(TAG, "Fail to encode domain name and stop encoding", e);
return null;
}
}
/**
* Decode domain name(s) from the given byteBuffer. Decode follows RFC1035 section 3.1 and
* section 4.1.4(message compression).
*
* @return domain name(s) string array with space separated, or empty string if decode fails.
*/
@NonNull
public static String[] decode(@NonNull final ByteBuffer buffer, boolean compression) {
final ArrayList<String> domainList = new ArrayList<>();
while (buffer.remaining() > 0) {
try {
// TODO: replace the recursion with loop in parseName and don't need to pass in the
// maxLabelCount parameter to prevent recursion from overflowing stack.
final String domain = DnsRecordParser.parseName(buffer, 0 /* depth */,
15 /* maxLabelCount */, compression);
if (!DnsRecordParser.isHostName(domain)) continue;
domainList.add(domain);
} catch (BufferUnderflowException | DnsPacket.ParseException e) {
Log.e(TAG, "Fail to parse domain name and stop parsing", e);
break;
}
}
return domainList.toArray(new String[0]);
}
}

View file

@ -128,6 +128,15 @@ public final class DnsPacketUtils {
&& !InetAddresses.isNumericAddress(hostName);
}
/**
* Parses the domain / target name of a DNS record.
*/
public static String parseName(final ByteBuffer buf, int depth,
boolean isNameCompressionSupported) throws
BufferUnderflowException, DnsPacket.ParseException {
return parseName(buf, depth, MAXLABELCOUNT, isNameCompressionSupported);
}
/**
* Parses the domain / target name of a DNS record.
*
@ -136,10 +145,10 @@ public final class DnsPacketUtils {
* record may or may not support Name Compression, depending on the record TYPE. Moreover,
* even if Name Compression is supported, its usage is left to the implementation.
*/
public static String parseName(ByteBuffer buf, int depth,
public static String parseName(final ByteBuffer buf, int depth, int maxLabelCount,
boolean isNameCompressionSupported) throws
BufferUnderflowException, DnsPacket.ParseException {
if (depth > MAXLABELCOUNT) {
if (depth > maxLabelCount) {
throw new DnsPacket.ParseException("Failed to parse name, too many labels");
}
final int len = Byte.toUnsignedInt(buf.get());
@ -158,7 +167,8 @@ public final class DnsPacketUtils {
"Parse compression name fail, invalid compression");
}
buf.position(offset);
final String pointed = parseName(buf, depth + 1, isNameCompressionSupported);
final String pointed = parseName(buf, depth + 1, maxLabelCount,
isNameCompressionSupported);
buf.position(oldPos);
return pointed;
} else {
@ -168,7 +178,8 @@ public final class DnsPacketUtils {
if (head.length() > MAXLABELSIZE) {
throw new DnsPacket.ParseException("Parse name fail, invalid label length");
}
final String tail = parseName(buf, depth + 1, isNameCompressionSupported);
final String tail = parseName(buf, depth + 1, maxLabelCount,
isNameCompressionSupported);
return TextUtils.isEmpty(tail) ? head : head + "." + tail;
}
}

View file

@ -0,0 +1,206 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.net.module.util;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import libcore.util.HexEncoding;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.nio.ByteBuffer;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class DomainUtilsTest {
@Test
public void testEncodeInvalidDomain() {
byte[] buffer = DomainUtils.encode(".google.com");
assertNull(buffer);
buffer = DomainUtils.encode("google.com.");
assertNull(buffer);
buffer = DomainUtils.encode("-google.com");
assertNull(buffer);
buffer = DomainUtils.encode("google.com-");
assertNull(buffer);
buffer = DomainUtils.encode("google..com");
assertNull(buffer);
buffer = DomainUtils.encode("google!.com");
assertNull(buffer);
buffer = DomainUtils.encode("google.o");
assertNull(buffer);
buffer = DomainUtils.encode("google,com");
assertNull(buffer);
}
@Test
public void testEncodeValidDomainNamesWithoutCompression() {
// Single domain: "google.com"
String suffix = "06676F6F676C6503636F6D00";
byte[] buffer = DomainUtils.encode("google.com");
//assertNotNull(buffer);
assertEquals(suffix, HexEncoding.encodeToString(buffer));
// Single domain: "google-guest.com"
suffix = "0C676F6F676C652D677565737403636F6D00";
buffer = DomainUtils.encode("google-guest.com");
assertNotNull(buffer);
assertEquals(suffix, HexEncoding.encodeToString(buffer));
// domain search list: "example.corp.google.com", "corp.google.com", "google.com"
suffix = "076578616D706C6504636F727006676F6F676C6503636F6D00" // example.corp.google.com
+ "04636F727006676F6F676C6503636F6D00" // corp.google.com
+ "06676F6F676C6503636F6D00"; // google.com
buffer = DomainUtils.encode(new String[] {
"example.corp.google.com", "corp.google.com", "google.com"},
false /* compression */);
assertNotNull(buffer);
assertEquals(suffix, HexEncoding.encodeToString(buffer));
// domain search list: "example.corp.google.com", "corp..google.com"(invalid domain),
// "google.com"
suffix = "076578616D706C6504636F727006676F6F676C6503636F6D00" // example.corp.google.com
+ "06676F6F676C6503636F6D00"; // google.com
buffer = DomainUtils.encode(new String[] {
"example.corp.google.com", "corp..google.com", "google.com"},
false /* compression */);
assertNotNull(buffer);
assertEquals(suffix, HexEncoding.encodeToString(buffer));
// Invalid domain search list: "corp..google.com", "..google.com"
buffer = DomainUtils.encode(new String[] {"corp..google.com", "..google.com"},
false /* compression */);
assertEquals(0, buffer.length);
}
@Test
public void testEncodeValidDomainNamesWithCompression() {
// domain search list: "example.corp.google.com", "corp.google.com", "google.com"
String suffix =
"076578616D706C6504636F727006676F6F676C6503636F6D00" // example.corp.google.com
+ "C008" // corp.google.com
+ "C00D"; // google.com
byte[] buffer = DomainUtils.encode(new String[] {
"example.corp.google.com", "corp.google.com", "google.com"}, true);
assertNotNull(buffer);
assertEquals(suffix, HexEncoding.encodeToString(buffer));
// domain search list: "example.corp.google.com", "a.example.corp.google.com", "google.com"
suffix = "076578616D706C6504636F727006676F6F676C6503636F6D00" // example.corp.google.com
+ "0161C000" // a.example.corp.google.com
+ "C00D"; // google.com
buffer = DomainUtils.encode(new String[] {
"example.corp.google.com", "a.example.corp.google.com", "google.com"}, true);
assertNotNull(buffer);
assertEquals(suffix, HexEncoding.encodeToString(buffer));
// domain search list: "example.corp.google.com", "google.com", "gle.com"
suffix = "076578616D706C6504636F727006676F6F676C6503636F6D00" // example.corp.google.com
+ "C00D" // google.com
+ "03676C65C014"; // gle.com
buffer = DomainUtils.encode(new String[] {
"example.corp.google.com", "google.com", "gle.com"}, true);
assertNotNull(buffer);
assertEquals(suffix, HexEncoding.encodeToString(buffer));
// domain search list: "example.corp.google.com", "google.com", "google"
suffix = "076578616D706C6504636F727006676F6F676C6503636F6D00" // example.corp.google.com
+ "C00D"; // google.com
buffer = DomainUtils.encode(new String[] {
"example.corp.google.com", "google.com", "google"}, true);
assertNotNull(buffer);
assertEquals(suffix, HexEncoding.encodeToString(buffer));
// domain search list: "example.corp.google.com", "..google.com"(invalid domain), "google"
suffix = "076578616D706C6504636F727006676F6F676C6503636F6D00"; // example.corp.google.com
buffer = DomainUtils.encode(new String[] {
"example.corp.google.com", "..google.com", "google"}, true);
assertNotNull(buffer);
assertEquals(suffix, HexEncoding.encodeToString(buffer));
// domain search list: "example.corp.google.com", "suffix.example.edu.cn", "edu.cn"
suffix = "076578616D706C6504636F727006676F6F676C6503636F6D00" // example.corp.google.com
+ "06737566666978076578616D706C650365647502636E00" // suffix.example.edu.cn
+ "C028"; // edu.cn
buffer = DomainUtils.encode(new String[] {
"example.corp.google.com", "suffix.example.edu.cn", "edu.cn"}, true);
assertNotNull(buffer);
assertEquals(suffix, HexEncoding.encodeToString(buffer));
// domain search list: "google.com", "example.com", "sub.example.com"
suffix = "06676F6F676C6503636F6D00" // google.com
+ "076578616D706C65C007" // example.com
+ "03737562C00C"; // sub.example.com
buffer = DomainUtils.encode(new String[] {
"google.com", "example.com", "sub.example.com"}, true);
assertNotNull(buffer);
assertEquals(suffix, HexEncoding.encodeToString(buffer));
}
@Test
public void testDecodeDomainNames() {
String suffixes = "06676F6F676C6503636F6D00" // google.com
+ "076578616D706C6503636F6D00" // example.com
+ "06676F6F676C6500"; // google
String[] expected = new String[] {"google.com", "example.com"};
ByteBuffer buffer = ByteBuffer.wrap(HexEncoding.decode(suffixes));
String[] suffixString = DomainUtils.decode(buffer, false /* compression */);
assertArrayEquals(expected, suffixString);
// include suffix with invalid length: 64
suffixes = "06676F6F676C6503636F6D00" // google.com
+ "406578616D706C6503636F6D00" // example.com(length=64)
+ "06676F6F676C6500"; // google
expected = new String[] {"google.com"};
buffer = ByteBuffer.wrap(HexEncoding.decode(suffixes));
suffixString = DomainUtils.decode(buffer, false /* compression */);
assertArrayEquals(expected, suffixString);
// include suffix with invalid length: 0
suffixes = "06676F6F676C6503636F6D00" // google.com
+ "076578616D706C6503636F6D00" // example.com
+ "00676F6F676C6500"; // google(length=0)
expected = new String[] {"google.com", "example.com"};
buffer = ByteBuffer.wrap(HexEncoding.decode(suffixes));
suffixString = DomainUtils.decode(buffer, false /* compression */);
assertArrayEquals(expected, suffixString);
suffixes =
"076578616D706C6504636F727006676F6F676C6503636F6D00" // example.corp.google.com
+ "C008" // corp.google.com
+ "C00D"; // google.com
expected = new String[] {"example.corp.google.com", "corp.google.com", "google.com"};
buffer = ByteBuffer.wrap(HexEncoding.decode(suffixes));
suffixString = DomainUtils.decode(buffer, true /* compression */);
assertArrayEquals(expected, suffixString);
}
}