Add test utils for SVCB packet generation.
This is based on patchset 22 of aosp/2453209.
Bug: 240259333
Test: test-only code, will be covered by NetworkMonitorTest
Change-Id: Ibede3fb519e6f1d0c3d99886f553ee006ddfebc9
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DnsSvcbUtils.java b/staticlibs/testutils/devicetests/com/android/testutils/DnsSvcbUtils.java
new file mode 100644
index 0000000..8608344
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DnsSvcbUtils.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2024 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.testutils;
+
+import static android.net.DnsResolver.CLASS_IN;
+
+import static com.android.net.module.util.DnsPacket.TYPE_SVCB;
+import static com.android.net.module.util.DnsPacketUtils.DnsRecordParser.domainNameToLabels;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN;
+
+import static org.junit.Assert.fail;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+
+import android.net.InetAddresses;
+
+import androidx.annotation.NonNull;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class DnsSvcbUtils {
+ private static final Pattern SVC_PARAM_PATTERN = Pattern.compile("([a-z0-9-]+)=?(.*)");
+
+ /**
+ * Returns a DNS SVCB response with given hostname `hostname` and given SVCB records
+ * `records`. Each record must contain the service priority, the target name, and the service
+ * parameters.
+ * E.g. "1 doh.google alpn=h2,h3 port=443 ipv4hint=192.0.2.1 dohpath=/dns-query{?dns}"
+ */
+ @NonNull
+ public static byte[] makeSvcbResponse(String hostname, String[] records) throws IOException {
+ if (records == null) throw new NullPointerException();
+ if (!hostname.startsWith("_dns.")) throw new UnsupportedOperationException();
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ // Write DNS header.
+ os.write(shortsToByteArray(
+ 0x1234, /* Transaction ID */
+ 0x8100, /* Flags */
+ 1, /* qdcount */
+ records.length, /* ancount */
+ 0, /* nscount */
+ 0 /* arcount */
+ ));
+ // Write Question.
+ // - domainNameToLabels() doesn't support the hostname starting with "_", so divide
+ // the writing into two steps.
+ os.write(new byte[] { 0x04, '_', 'd', 'n', 's' });
+ os.write(domainNameToLabels(hostname.substring(5)));
+ os.write(shortsToByteArray(TYPE_SVCB, CLASS_IN));
+ // Write Answer section.
+ for (String r : records) {
+ os.write(makeSvcbRecord(r));
+ }
+ return os.toByteArray();
+ }
+
+ @NonNull
+ private static byte[] makeSvcbRecord(String representation) throws IOException {
+ if (representation == null) return new byte[0];
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ os.write(shortsToByteArray(
+ 0xc00c, /* Pointer to qname in question section */
+ TYPE_SVCB,
+ CLASS_IN,
+ 0, 16, /* TTL = 16 */
+ 0 /* Data Length = 0 */
+
+ ));
+ final String[] strings = representation.split(" +");
+ // SvcPriority and TargetName are mandatory in the representation.
+ if (strings.length < 3) {
+ fail("Invalid SVCB representation: " + representation);
+ }
+ // Write SvcPriority, TargetName, and SvcParams.
+ os.write(shortsToByteArray(Short.parseShort(strings[0])));
+ os.write(domainNameToLabels(strings[1]));
+ for (int i = 2; i < strings.length; i++) {
+ try {
+ os.write(svcParamToByteArray(strings[i]));
+ } catch (UnsupportedEncodingException e) {
+ throw new IOException(e);
+ }
+ }
+ // Update rdata length.
+ final byte[] out = os.toByteArray();
+ ByteBuffer.wrap(out).putShort(10, (short) (out.length - 12));
+ return out;
+ }
+
+ @NonNull
+ private static byte[] svcParamToByteArray(String svcParam) throws IOException {
+ final Matcher matcher = SVC_PARAM_PATTERN.matcher(svcParam);
+ if (!matcher.matches() || matcher.groupCount() != 2) {
+ fail("Invalid SvcParam: " + svcParam);
+ }
+ final String svcParamkey = matcher.group(1);
+ final String svcParamValue = matcher.group(2);
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ os.write(svcParamKeyToBytes(svcParamkey));
+ switch (svcParamkey) {
+ case "mandatory":
+ final String[] keys = svcParamValue.split(",");
+ os.write(shortsToByteArray(keys.length));
+ for (String v : keys) {
+ os.write(svcParamKeyToBytes(v));
+ }
+ break;
+ case "alpn":
+ os.write(shortsToByteArray((svcParamValue.length() + 1)));
+ for (String v : svcParamValue.split(",")) {
+ os.write(v.length());
+ // TODO: support percent-encoding per RFC 7838.
+ os.write(v.getBytes(US_ASCII));
+ }
+ break;
+ case "no-default-alpn":
+ os.write(shortsToByteArray(0));
+ break;
+ case "port":
+ os.write(shortsToByteArray(2));
+ os.write(shortsToByteArray(Short.parseShort(svcParamValue)));
+ break;
+ case "ipv4hint":
+ final String[] v4Addrs = svcParamValue.split(",");
+ os.write(shortsToByteArray((v4Addrs.length * IPV4_ADDR_LEN)));
+ for (String v : v4Addrs) {
+ os.write(InetAddresses.parseNumericAddress(v).getAddress());
+ }
+ break;
+ case "ech":
+ os.write(shortsToByteArray(svcParamValue.length()));
+ os.write(svcParamValue.getBytes(US_ASCII)); // base64 encoded
+ break;
+ case "ipv6hint":
+ final String[] v6Addrs = svcParamValue.split(",");
+ os.write(shortsToByteArray((v6Addrs.length * IPV6_ADDR_LEN)));
+ for (String v : v6Addrs) {
+ os.write(InetAddresses.parseNumericAddress(v).getAddress());
+ }
+ break;
+ case "dohpath":
+ os.write(shortsToByteArray(svcParamValue.length()));
+ // TODO: support percent-encoding, since this is a URI template.
+ os.write(svcParamValue.getBytes(US_ASCII));
+ break;
+ default:
+ os.write(shortsToByteArray(svcParamValue.length()));
+ os.write(svcParamValue.getBytes(US_ASCII));
+ break;
+ }
+ return os.toByteArray();
+ }
+
+ @NonNull
+ private static byte[] svcParamKeyToBytes(String key) {
+ switch (key) {
+ case "mandatory": return shortsToByteArray(0);
+ case "alpn": return shortsToByteArray(1);
+ case "no-default-alpn": return shortsToByteArray(2);
+ case "port": return shortsToByteArray(3);
+ case "ipv4hint": return shortsToByteArray(4);
+ case "ech": return shortsToByteArray(5);
+ case "ipv6hint": return shortsToByteArray(6);
+ case "dohpath": return shortsToByteArray(7);
+ default:
+ if (!key.startsWith("key")) fail("Invalid SvcParamKey " + key);
+ return shortsToByteArray(Short.parseShort(key.substring(3)));
+ }
+ }
+
+ @NonNull
+ private static byte[] shortsToByteArray(int... values) {
+ final ByteBuffer out = ByteBuffer.allocate(values.length * 2);
+ for (int value: values) {
+ if (value < 0 || value > 0xffff) {
+ throw new AssertionError("not an unsigned short: " + value);
+ }
+ out.putShort((short) value);
+ }
+ return out.array();
+ }
+}