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:
parent
2ca9f07b99
commit
cece401526
4 changed files with 365 additions and 4 deletions
|
|
@ -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",
|
||||
|
|
|
|||
143
staticlibs/device/com/android/net/module/util/DomainUtils.java
Normal file
143
staticlibs/device/com/android/net/module/util/DomainUtils.java
Normal 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]);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue