Merge "Move ticksToMilliSeconds to NetlinkUtils." into main
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index ff65228..e1b5601 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -35,6 +35,7 @@
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",
diff --git a/staticlibs/device/com/android/net/module/util/DomainUtils.java b/staticlibs/device/com/android/net/module/util/DomainUtils.java
new file mode 100644
index 0000000..80e0b64
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/DomainUtils.java
@@ -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]);
+ }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/DnsPacketUtils.java b/staticlibs/framework/com/android/net/module/util/DnsPacketUtils.java
index c47bfa0..105d783 100644
--- a/staticlibs/framework/com/android/net/module/util/DnsPacketUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/DnsPacketUtils.java
@@ -130,16 +130,25 @@
/**
* 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.
*
* As described in RFC 1035 Section 4.1.3, the NAME field of a DNS Resource Record always
* supports Name Compression, whereas domain names contained in the RDATA payload of a DNS
* 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 @@
"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 @@
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;
}
}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/DomainUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/DomainUtilsTest.java
new file mode 100644
index 0000000..606ed5f
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/DomainUtilsTest.java
@@ -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);
+ }
+}