Merge changes from topic "dns_svcb" into main

* changes:
  Add tests for DnsSvcbRecord and DnsSvcbPacket
  Add DnsSvcbPacket
  Add DnsSvcbRecord
This commit is contained in:
Mike Yu 2023-11-09 05:24:17 +00:00 • committed by Gerrit Code Review
commit d97fdbb3bc
6 changed files with 1337 additions and 1 deletions

View file

@ -96,12 +96,17 @@ java_library {
srcs: [
"framework/**/DnsPacket.java",
"framework/**/DnsPacketUtils.java",
"framework/**/DnsSvcbPacket.java",
"framework/**/DnsSvcbRecord.java",
"framework/**/HexDump.java",
"framework/**/NetworkStackConstants.java",
],
sdk_version: "module_current",
visibility: [
"//packages/services/Iwlan:__subpackages__",
],
libs: [
"androidx.annotation_annotation",
"framework-annotations-lib",
"framework-connectivity.stubs.module_lib",
],

View file

@ -56,6 +56,7 @@ public abstract class DnsPacket {
*/
// TODO: Define the constant as a public constant in DnsResolver since it can never change.
private static final int TYPE_CNAME = 5;
public static final int TYPE_SVCB = 64;
/**
* Thrown when parsing packet failed.
@ -282,7 +283,7 @@ public abstract class DnsPacket {
* @param buf ByteBuffer input of record, must be in network byte order
* (which is the default).
*/
private DnsRecord(@RecordType int rType, @NonNull ByteBuffer buf)
protected DnsRecord(@RecordType int rType, @NonNull ByteBuffer buf)
throws BufferUnderflowException, ParseException {
Objects.requireNonNull(buf);
this.rType = rType;
@ -326,6 +327,8 @@ public abstract class DnsPacket {
// Return a DnsRecord instance by default for backward compatibility, this is useful
// when a partner supports new type of DnsRecord but does not inherit DnsRecord.
switch (nsType) {
case TYPE_SVCB:
return new DnsSvcbRecord(rType, buf);
default:
return new DnsRecord(rType, buf);
}

View file

@ -0,0 +1,182 @@
/*
* 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 android.net.DnsResolver.TYPE_A;
import static android.net.DnsResolver.TYPE_AAAA;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.StringJoiner;
/**
* A class for a DNS SVCB response packet.
*
* @hide
*/
public class DnsSvcbPacket extends DnsPacket {
public static final int TYPE_SVCB = 64;
private static final String TAG = DnsSvcbPacket.class.getSimpleName();
/**
* Creates a DnsSvcbPacket object from the given wire-format DNS packet.
*/
private DnsSvcbPacket(@NonNull byte[] data) throws DnsPacket.ParseException {
// If data is null, ParseException will be thrown.
super(data);
final int questions = mHeader.getRecordCount(QDSECTION);
if (questions != 1) {
throw new DnsPacket.ParseException("Unexpected question count " + questions);
}
final int nsType = mRecords[QDSECTION].get(0).nsType;
if (nsType != TYPE_SVCB) {
throw new DnsPacket.ParseException("Unexpected query type " + nsType);
}
}
/**
* Returns true if the DnsSvcbPacket is a DNS response.
*/
public boolean isResponse() {
return mHeader.isResponse();
}
/**
* Returns whether the given protocol alpn is supported.
*/
public boolean isSupported(@NonNull String alpn) {
return findSvcbRecord(alpn) != null;
}
/**
* Returns the TargetName associated with the given protocol alpn.
* If the alpn is not supported, a null is returned.
*/
@Nullable
public String getTargetName(@NonNull String alpn) {
final DnsSvcbRecord record = findSvcbRecord(alpn);
return (record != null) ? record.getTargetName() : null;
}
/**
* Returns the TargetName that associated with the given protocol alpn.
* If the alpn is not supported, -1 is returned.
*/
public int getPort(@NonNull String alpn) {
final DnsSvcbRecord record = findSvcbRecord(alpn);
return (record != null) ? record.getPort() : -1;
}
/**
* Returns the IP addresses that support the given protocol alpn.
* If the alpn is not supported, an empty list is returned.
*/
@NonNull
public List<InetAddress> getAddresses(@NonNull String alpn) {
final DnsSvcbRecord record = findSvcbRecord(alpn);
if (record == null) return Collections.EMPTY_LIST;
// As per draft-ietf-dnsop-svcb-https-10#section-7.4 and draft-ietf-add-ddr-10#section-4,
// if A/AAAA records are available in the Additional section, use the IP addresses in
// those records instead of the IP addresses in ipv4hint/ipv6hint.
final List<InetAddress> out = getAddressesFromAdditionalSection();
if (out.size() > 0) return out;
return record.getAddresses();
}
/**
* Returns the value of SVCB key dohpath that associated with the given protocol alpn.
* If the alpn is not supported, a null is returned.
*/
@Nullable
public String getDohPath(@NonNull String alpn) {
final DnsSvcbRecord record = findSvcbRecord(alpn);
return (record != null) ? record.getDohPath() : null;
}
/**
* Returns the DnsSvcbRecord associated with the given protocol alpn.
* If the alpn is not supported, a null is returned.
*/
@Nullable
private DnsSvcbRecord findSvcbRecord(@NonNull String alpn) {
for (final DnsRecord record : mRecords[ANSECTION]) {
if (record instanceof DnsSvcbRecord) {
final DnsSvcbRecord svcbRecord = (DnsSvcbRecord) record;
if (svcbRecord.getAlpns().contains(alpn)) {
return svcbRecord;
}
}
}
return null;
}
/**
* Returns the IP addresses in additional section.
*/
@NonNull
private List<InetAddress> getAddressesFromAdditionalSection() {
final List<InetAddress> out = new ArrayList<InetAddress>();
if (mHeader.getRecordCount(ARSECTION) == 0) {
return out;
}
for (final DnsRecord record : mRecords[ARSECTION]) {
if (record.nsType != TYPE_A && record.nsType != TYPE_AAAA) {
Log.d(TAG, "Found type other than A/AAAA in Additional section: " + record.nsType);
continue;
}
try {
out.add(InetAddress.getByAddress(record.getRR()));
} catch (UnknownHostException e) {
Log.w(TAG, "Failed to parse address");
}
}
return out;
}
@Override
public String toString() {
final StringJoiner out = new StringJoiner(" ");
out.add("QUERY: [" + TextUtils.join(", ", mRecords[QDSECTION]) + "]");
out.add("ANSWER: [" + TextUtils.join(", ", mRecords[ANSECTION]) + "]");
out.add("AUTHORITY: [" + TextUtils.join(", ", mRecords[NSSECTION]) + "]");
out.add("ADDITIONAL: [" + TextUtils.join(", ", mRecords[ARSECTION]) + "]");
return out.toString();
}
/**
* Creates a DnsSvcbPacket object from the given wire-format DNS answer.
*/
public static DnsSvcbPacket fromResponse(@NonNull byte[] data) throws DnsPacket.ParseException {
DnsSvcbPacket out = new DnsSvcbPacket(data);
if (!out.isResponse()) {
throw new DnsPacket.ParseException("Not an answer packet");
}
return out;
}
}

View file

@ -0,0 +1,516 @@
/*
* 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 android.net.DnsResolver.CLASS_IN;
import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
import static com.android.net.module.util.DnsPacket.ParseException;
import android.annotation.NonNull;
import android.text.TextUtils;
import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ShortBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.StringJoiner;
/**
* A class for an SVCB record.
* https://www.iana.org/assignments/dns-svcb/dns-svcb.xhtml
* @hide
*/
@VisibleForTesting(visibility = PACKAGE)
public final class DnsSvcbRecord extends DnsPacket.DnsRecord {
/**
* The following SvcParamKeys KEY_* are defined in
* https://www.iana.org/assignments/dns-svcb/dns-svcb.xhtml.
*/
// The SvcParamKey "mandatory". The associated implementation of SvcParam is SvcParamMandatory.
private static final int KEY_MANDATORY = 0;
// The SvcParamKey "alpn". The associated implementation of SvcParam is SvcParamAlpn.
private static final int KEY_ALPN = 1;
// The SvcParamKey "no-default-alpn". The associated implementation of SvcParam is
// SvcParamNoDefaultAlpn.
private static final int KEY_NO_DEFAULT_ALPN = 2;
// The SvcParamKey "port". The associated implementation of SvcParam is SvcParamPort.
private static final int KEY_PORT = 3;
// The SvcParamKey "ipv4hint". The associated implementation of SvcParam is SvcParamIpv4Hint.
private static final int KEY_IPV4HINT = 4;
// The SvcParamKey "ech". The associated implementation of SvcParam is SvcParamEch.
private static final int KEY_ECH = 5;
// The SvcParamKey "ipv6hint". The associated implementation of SvcParam is SvcParamIpv6Hint.
private static final int KEY_IPV6HINT = 6;
// The SvcParamKey "dohpath". The associated implementation of SvcParam is SvcParamDohPath.
private static final int KEY_DOHPATH = 7;
// The minimal size of a SvcParam.
// https://www.ietf.org/archive/id/draft-ietf-dnsop-svcb-https-12.html#name-rdata-wire-format
private static final int MINSVCPARAMSIZE = 4;
private static final String TAG = DnsSvcbRecord.class.getSimpleName();
private final int mSvcPriority;
@NonNull
private final String mTargetName;
@NonNull
private final SparseArray<SvcParam> mAllSvcParams = new SparseArray<>();
@VisibleForTesting(visibility = PACKAGE)
public DnsSvcbRecord(@DnsPacket.RecordType int rType, @NonNull ByteBuffer buff)
throws IllegalStateException, ParseException {
super(rType, buff);
if (nsType != DnsPacket.TYPE_SVCB) {
throw new IllegalStateException("incorrect nsType: " + nsType);
}
if (nsClass != CLASS_IN) {
throw new ParseException("incorrect nsClass: " + nsClass);
}
// DNS Record in Question Section doesn't have Rdata.
if (rType == DnsPacket.QDSECTION) {
mSvcPriority = 0;
mTargetName = "";
return;
}
final byte[] rdata = getRR();
if (rdata == null) {
throw new ParseException("SVCB rdata is empty");
}
final ByteBuffer buf = ByteBuffer.wrap(rdata).asReadOnlyBuffer();
mSvcPriority = Short.toUnsignedInt(buf.getShort());
mTargetName = DnsPacketUtils.DnsRecordParser.parseName(buf, 0 /* Parse depth */,
false /* isNameCompressionSupported */);
if (mTargetName.length() > DnsPacket.DnsRecord.MAXNAMESIZE) {
throw new ParseException(
"Failed to parse SVCB target name, name size is too long: "
+ mTargetName.length());
}
while (buf.remaining() >= MINSVCPARAMSIZE) {
final SvcParam svcParam = parseSvcParam(buf);
final int key = svcParam.getKey();
if (mAllSvcParams.get(key) != null) {
throw new ParseException("Invalid DnsSvcbRecord, key " + key + " is repeated");
}
mAllSvcParams.put(key, svcParam);
}
if (buf.hasRemaining()) {
throw new ParseException("Invalid DnsSvcbRecord. Got "
+ buf.remaining() + " remaining bytes after parsing");
}
}
/**
* Returns the TargetName.
*/
@VisibleForTesting(visibility = PACKAGE)
@NonNull
public String getTargetName() {
return mTargetName;
}
/**
* Returns an unmodifiable list of alpns from SvcParam alpn.
*/
@VisibleForTesting(visibility = PACKAGE)
@NonNull
public List<String> getAlpns() {
final SvcParamAlpn sp = (SvcParamAlpn) mAllSvcParams.get(KEY_ALPN);
final List<String> list = (sp != null) ? sp.getValue() : Collections.EMPTY_LIST;
return Collections.unmodifiableList(list);
}
/**
* Returns the port number from SvcParam port.
*/
@VisibleForTesting(visibility = PACKAGE)
public int getPort() {
final SvcParamPort sp = (SvcParamPort) mAllSvcParams.get(KEY_PORT);
return (sp != null) ? sp.getValue() : -1;
}
/**
* Returns a list of the IP addresses from both of SvcParam ipv4hint and ipv6hint.
*/
@VisibleForTesting(visibility = PACKAGE)
@NonNull
public List<InetAddress> getAddresses() {
final List<InetAddress> out = new ArrayList<>();
final SvcParamIpHint sp4 = (SvcParamIpHint) mAllSvcParams.get(KEY_IPV4HINT);
if (sp4 != null) {
out.addAll(sp4.getValue());
}
final SvcParamIpHint sp6 = (SvcParamIpHint) mAllSvcParams.get(KEY_IPV6HINT);
if (sp6 != null) {
out.addAll(sp6.getValue());
}
return out;
}
/**
* Returns the doh path from SvcParam dohPath.
*/
@VisibleForTesting(visibility = PACKAGE)
@NonNull
public String getDohPath() {
final SvcParamDohPath sp = (SvcParamDohPath) mAllSvcParams.get(KEY_DOHPATH);
return (sp != null) ? sp.getValue() : "";
}
@Override
public String toString() {
if (rType == DnsPacket.QDSECTION) {
return dName + " IN SVCB";
}
final StringJoiner sj = new StringJoiner(" ");
for (int i = 0; i < mAllSvcParams.size(); i++) {
sj.add(mAllSvcParams.valueAt(i).toString());
}
return dName + " " + ttl + " IN SVCB " + mSvcPriority + " " + mTargetName + " "
+ sj.toString();
}
private static SvcParam parseSvcParam(@NonNull ByteBuffer buf) throws ParseException {
try {
final int key = Short.toUnsignedInt(buf.getShort());
switch (key) {
case KEY_MANDATORY: return new SvcParamMandatory(buf);
case KEY_ALPN: return new SvcParamAlpn(buf);
case KEY_NO_DEFAULT_ALPN: return new SvcParamNoDefaultAlpn(buf);
case KEY_PORT: return new SvcParamPort(buf);
case KEY_IPV4HINT: return new SvcParamIpv4Hint(buf);
case KEY_ECH: return new SvcParamEch(buf);
case KEY_IPV6HINT: return new SvcParamIpv6Hint(buf);
case KEY_DOHPATH: return new SvcParamDohPath(buf);
default: return new SvcParamGeneric(key, buf);
}
} catch (BufferUnderflowException e) {
throw new ParseException("Malformed packet", e);
}
}
/**
* The base class for all SvcParam.
*/
private abstract static class SvcParam {
private final int mKey;
SvcParam(int key) {
mKey = key;
}
int getKey() {
return mKey;
}
}
private static class SvcParamMandatory extends SvcParam {
private final short[] mValue;
private SvcParamMandatory(@NonNull ByteBuffer buf) throws BufferUnderflowException,
ParseException {
super(KEY_MANDATORY);
// The caller already read 2 bytes for SvcParamKey.
final int len = Short.toUnsignedInt(buf.getShort());
final ByteBuffer svcParamValue = sliceAndAdvance(buf, len);
mValue = SvcParamValueUtil.toShortArray(svcParamValue);
if (mValue.length == 0) {
throw new ParseException("mandatory value must be non-empty");
}
}
@Override
public String toString() {
final StringJoiner valueJoiner = new StringJoiner(",");
for (short key : mValue) {
valueJoiner.add(toKeyName(key));
}
return toKeyName(getKey()) + "=" + valueJoiner.toString();
}
}
private static class SvcParamAlpn extends SvcParam {
private final List<String> mValue;
SvcParamAlpn(@NonNull ByteBuffer buf) throws BufferUnderflowException, ParseException {
super(KEY_ALPN);
// The caller already read 2 bytes for SvcParamKey.
final int len = Short.toUnsignedInt(buf.getShort());
final ByteBuffer svcParamValue = sliceAndAdvance(buf, len);
mValue = SvcParamValueUtil.toStringList(svcParamValue);
if (mValue.isEmpty()) {
throw new ParseException("alpn value must be non-empty");
}
}
List<String> getValue() {
return Collections.unmodifiableList(mValue);
}
@Override
public String toString() {
return toKeyName(getKey()) + "=" + TextUtils.join(",", mValue);
}
}
private static class SvcParamNoDefaultAlpn extends SvcParam {
SvcParamNoDefaultAlpn(@NonNull ByteBuffer buf) throws BufferUnderflowException,
ParseException {
super(KEY_NO_DEFAULT_ALPN);
// The caller already read 2 bytes for SvcParamKey.
final int len = buf.getShort();
if (len != 0) {
throw new ParseException("no-default-alpn value must be empty");
}
}
@Override
public String toString() {
return toKeyName(getKey());
}
}
private static class SvcParamPort extends SvcParam {
private final int mValue;
SvcParamPort(@NonNull ByteBuffer buf) throws BufferUnderflowException, ParseException {
super(KEY_PORT);
// The caller already read 2 bytes for SvcParamKey.
final int len = buf.getShort();
if (len != Short.BYTES) {
throw new ParseException("key port len is not 2 but " + len);
}
mValue = Short.toUnsignedInt(buf.getShort());
}
int getValue() {
return mValue;
}
@Override
public String toString() {
return toKeyName(getKey()) + "=" + mValue;
}
}
private static class SvcParamIpHint extends SvcParam {
private final List<InetAddress> mValue;
private SvcParamIpHint(int key, @NonNull ByteBuffer buf, int addrLen) throws
BufferUnderflowException, ParseException {
super(key);
// The caller already read 2 bytes for SvcParamKey.
final int len = Short.toUnsignedInt(buf.getShort());
final ByteBuffer svcParamValue = sliceAndAdvance(buf, len);
mValue = SvcParamValueUtil.toInetAddressList(svcParamValue, addrLen);
if (mValue.isEmpty()) {
throw new ParseException(toKeyName(getKey()) + " value must be non-empty");
}
}
List<InetAddress> getValue() {
return Collections.unmodifiableList(mValue);
}
@Override
public String toString() {
final StringJoiner valueJoiner = new StringJoiner(",");
for (InetAddress ip : mValue) {
valueJoiner.add(ip.getHostAddress());
}
return toKeyName(getKey()) + "=" + valueJoiner.toString();
}
}
private static class SvcParamIpv4Hint extends SvcParamIpHint {
SvcParamIpv4Hint(@NonNull ByteBuffer buf) throws BufferUnderflowException, ParseException {
super(KEY_IPV4HINT, buf, NetworkStackConstants.IPV4_ADDR_LEN);
}
}
private static class SvcParamIpv6Hint extends SvcParamIpHint {
SvcParamIpv6Hint(@NonNull ByteBuffer buf) throws BufferUnderflowException, ParseException {
super(KEY_IPV6HINT, buf, NetworkStackConstants.IPV6_ADDR_LEN);
}
}
private static class SvcParamEch extends SvcParamGeneric {
SvcParamEch(@NonNull ByteBuffer buf) throws BufferUnderflowException, ParseException {
super(KEY_ECH, buf);
}
}
private static class SvcParamDohPath extends SvcParam {
private final String mValue;
SvcParamDohPath(@NonNull ByteBuffer buf) throws BufferUnderflowException, ParseException {
super(KEY_DOHPATH);
// The caller already read 2 bytes for SvcParamKey.
final int len = Short.toUnsignedInt(buf.getShort());
final byte[] value = new byte[len];
buf.get(value);
mValue = new String(value, StandardCharsets.UTF_8);
}
String getValue() {
return mValue;
}
@Override
public String toString() {
return toKeyName(getKey()) + "=" + mValue;
}
}
// For other unrecognized and unimplemented SvcParams, they are stored as SvcParamGeneric.
private static class SvcParamGeneric extends SvcParam {
private final byte[] mValue;
SvcParamGeneric(int key, @NonNull ByteBuffer buf) throws BufferUnderflowException,
ParseException {
super(key);
// The caller already read 2 bytes for SvcParamKey.
final int len = Short.toUnsignedInt(buf.getShort());
mValue = new byte[len];
buf.get(mValue);
}
@Override
public String toString() {
final StringBuilder out = new StringBuilder();
out.append(toKeyName(getKey()));
if (mValue != null && mValue.length > 0) {
out.append("=");
out.append(HexDump.toHexString(mValue));
}
return out.toString();
}
}
private static String toKeyName(int key) {
switch (key) {
case KEY_MANDATORY: return "mandatory";
case KEY_ALPN: return "alpn";
case KEY_NO_DEFAULT_ALPN: return "no-default-alpn";
case KEY_PORT: return "port";
case KEY_IPV4HINT: return "ipv4hint";
case KEY_ECH: return "ech";
case KEY_IPV6HINT: return "ipv6hint";
case KEY_DOHPATH: return "dohpath";
default: return "key" + key;
}
}
/**
* Returns a read-only ByteBuffer (with position = 0, limit = `length`, and capacity = `length`)
* sliced from `buf`'s current position, and moves the position of `buf` by `length`.
*/
@VisibleForTesting(visibility = PRIVATE)
public static ByteBuffer sliceAndAdvance(@NonNull ByteBuffer buf, int length)
throws BufferUnderflowException {
if (buf.remaining() < length) {
throw new BufferUnderflowException();
}
final int pos = buf.position();
// `out` equals to `buf.slice(pos, length)` that is supported in API level 34.
final ByteBuffer out = ((ByteBuffer) buf.slice().limit(length)).slice();
buf.position(pos + length);
return out.asReadOnlyBuffer();
}
// A utility to convert the byte array of SvcParamValue to other types.
private static class SvcParamValueUtil {
// Refer to draft-ietf-dnsop-svcb-https-10#section-7.1 for the wire format of alpn.
@NonNull
private static List<String> toStringList(@NonNull ByteBuffer buf)
throws BufferUnderflowException, ParseException {
final List<String> out = new ArrayList<>();
while (buf.hasRemaining()) {
final int alpnLen = Byte.toUnsignedInt(buf.get());
if (alpnLen == 0) {
throw new ParseException("alpn should not be an empty string");
}
final byte[] alpn = new byte[alpnLen];
buf.get(alpn);
out.add(new String(alpn, StandardCharsets.UTF_8));
}
return out;
}
// Refer to draft-ietf-dnsop-svcb-https-10#section-7.5 for the wire format of SvcParamKey
// "mandatory".
@NonNull
private static short[] toShortArray(@NonNull ByteBuffer buf)
throws BufferUnderflowException, ParseException {
if (buf.remaining() % Short.BYTES != 0) {
throw new ParseException("Can't parse whole byte array");
}
final ShortBuffer sb = buf.asShortBuffer();
final short[] out = new short[sb.remaining()];
sb.get(out);
return out;
}
// Refer to draft-ietf-dnsop-svcb-https-10#section-7.4 for the wire format of ipv4hint and
// ipv6hint.
@NonNull
private static List<InetAddress> toInetAddressList(@NonNull ByteBuffer buf, int addrLen)
throws BufferUnderflowException, ParseException {
if (buf.remaining() % addrLen != 0) {
throw new ParseException("Can't parse whole byte array");
}
final List<InetAddress> out = new ArrayList<>();
final byte[] addr = new byte[addrLen];
while (buf.remaining() >= addrLen) {
buf.get(addr);
try {
out.add(InetAddress.getByAddress(addr));
} catch (UnknownHostException e) {
throw new ParseException("Can't parse byte array as an IP address");
}
}
return out;
}
}
}

View file

@ -203,6 +203,30 @@ public class DnsPacketTest {
"test.com", CLASS_IN, 0 /* ttl */, "example.com"));
}
/** Verifies that the type of implementation returned from DnsRecord#parse is correct */
@Test
public void testDnsRecordParse() throws IOException {
final byte[] svcbQuestionRecord = new byte[] {
0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 0x03, 'c', 'o', 'm', 0x00, /* Name */
0x00, 0x40, /* Type */
0x00, 0x01, /* Class */
};
assertTrue(DnsPacket.DnsRecord.parse(DnsPacket.QDSECTION,
ByteBuffer.wrap(svcbQuestionRecord)) instanceof DnsSvcbRecord);
final byte[] svcbAnswerRecord = new byte[] {
0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 0x03, 'c', 'o', 'm', 0x00, /* Name */
0x00, 0x40, /* Type */
0x00, 0x01, /* Class */
0x00, 0x00, 0x01, 0x2b, /* TTL */
0x00, 0x0b, /* Data length */
0x00, 0x01, /* SvcPriority */
0x03, 'd', 'o', 't', 0x03, 'c', 'o', 'm', 0x00, /* TargetName */
};
assertTrue(DnsPacket.DnsRecord.parse(DnsPacket.ANSECTION,
ByteBuffer.wrap(svcbAnswerRecord)) instanceof DnsSvcbRecord);
}
/**
* Verifies ttl/rData error handling when parsing
* {@link DnsPacket.DnsRecord} from bytes.

View file

@ -0,0 +1,606 @@
/*
* 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 android.net.DnsResolver.CLASS_IN;
import static android.net.DnsResolver.TYPE_A;
import static android.net.DnsResolver.TYPE_AAAA;
import static com.android.net.module.util.DnsPacket.TYPE_SVCB;
import static com.android.testutils.MiscAsserts.assertThrows;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import android.annotation.NonNull;
import android.net.InetAddresses;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@RunWith(JUnit4.class)
public class DnsSvcbPacketTest {
private static final short TEST_TRANSACTION_ID = 0x4321;
private static final byte[] TEST_DNS_RESPONSE_HEADER_FLAG = new byte[] { (byte) 0x81, 0x00 };
// A common DNS SVCB Question section with Name = "_dns.resolver.arpa".
private static final byte[] TEST_DNS_SVCB_QUESTION_SECTION = new byte[] {
0x04, '_', 'd', 'n', 's', 0x08, 'r', 'e', 's', 'o', 'l', 'v', 'e', 'r',
0x04, 'a', 'r', 'p', 'a', 0x00, 0x00, 0x40, 0x00, 0x01,
};
// mandatory=ipv4hint,alpn,key333
private static final byte[] TEST_SVC_PARAM_MANDATORY = new byte[] {
0x00, 0x00, 0x00, 0x06, 0x00, 0x04, 0x00, 0x01, 0x01, 0x4d,
};
// alpn=doq
private static final byte[] TEST_SVC_PARAM_ALPN_DOQ = new byte[] {
0x00, 0x01, 0x00, 0x04, 0x03, 'd', 'o', 'q'
};
// alpn=h2,http/1.1
private static final byte[] TEST_SVC_PARAM_ALPN_HTTPS = new byte[] {
0x00, 0x01, 0x00, 0x0c, 0x02, 'h', '2',
0x08, 'h', 't', 't', 'p', '/', '1', '.', '1',
};
// no-default-alpn
private static final byte[] TEST_SVC_PARAM_NO_DEFAULT_ALPN = new byte[] {
0x00, 0x02, 0x00, 0x00,
};
// port=5353
private static final byte[] TEST_SVC_PARAM_PORT = new byte[] {
0x00, 0x03, 0x00, 0x02, 0x14, (byte) 0xe9,
};
// ipv4hint=1.2.3.4,6.7.8.9
private static final byte[] TEST_SVC_PARAM_IPV4HINT_1 = new byte[] {
0x00, 0x04, 0x00, 0x08, 0x01, 0x02, 0x03, 0x04, 0x06, 0x07, 0x08, 0x09,
};
// ipv4hint=4.3.2.1
private static final byte[] TEST_SVC_PARAM_IPV4HINT_2 = new byte[] {
0x00, 0x04, 0x00, 0x04, 0x04, 0x03, 0x02, 0x01,
};
// ech=aBcDe
private static final byte[] TEST_SVC_PARAM_ECH = new byte[] {
0x00, 0x05, 0x00, 0x05, 'a', 'B', 'c', 'D', 'e',
};
// ipv6hint=2001:db8::1
private static final byte[] TEST_SVC_PARAM_IPV6HINT = new byte[] {
0x00, 0x06, 0x00, 0x10, 0x20, 0x01, 0x0d, (byte) 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
};
// dohpath=/some-path{?dns}
private static final byte[] TEST_SVC_PARAM_DOHPATH = new byte[] {
0x00, 0x07, 0x00, 0x10,
'/', 's', 'o', 'm', 'e', '-', 'p', 'a', 't', 'h', '{', '?', 'd', 'n', 's', '}',
};
// key12345=1A2B0C
private static final byte[] TEST_SVC_PARAM_GENERIC_WITH_VALUE = new byte[] {
0x30, 0x39, 0x00, 0x03, 0x1a, 0x2b, 0x0c,
};
// key12346
private static final byte[] TEST_SVC_PARAM_GENERIC_WITHOUT_VALUE = new byte[] {
0x30, 0x3a, 0x00, 0x00,
};
private static byte[] makeDnsResponseHeaderAsByteArray(int qdcount, int ancount, int nscount,
int arcount) {
final ByteBuffer buffer = ByteBuffer.wrap(new byte[12]);
buffer.putShort(TEST_TRANSACTION_ID); /* Transaction ID */
buffer.put(TEST_DNS_RESPONSE_HEADER_FLAG); /* Flags */
buffer.putShort((short) qdcount);
buffer.putShort((short) ancount);
buffer.putShort((short) nscount);
buffer.putShort((short) arcount);
return buffer.array();
}
private static DnsSvcbRecord makeDnsSvcbRecordFromByteArray(@NonNull byte[] data)
throws IOException {
return new DnsSvcbRecord(DnsPacket.ANSECTION, ByteBuffer.wrap(data));
}
private static DnsSvcbRecord makeDnsSvcbRecordWithSingleSvcParam(@NonNull byte[] svcParam)
throws IOException {
return makeDnsSvcbRecordFromByteArray(new TestDnsRecordByteArrayBuilder()
.setRRType(TYPE_SVCB)
.setTargetName("test.com")
.addRdata(svcParam)
.build());
}
// Converts a Short to a byte array in big endian.
private static byte[] shortToByteArray(short value) {
return new byte[] { (byte) (value >> 8), (byte) value };
}
private static byte[] getRemainingByteArray(@NonNull ByteBuffer buffer) {
final byte[] out = new byte[buffer.remaining()];
buffer.get(out);
return out;
}
// A utility to make a DNS record as byte array.
private static class TestDnsRecordByteArrayBuilder {
private static final byte[] NAME_COMPRESSION_POINTER = new byte[] { (byte) 0xc0, 0x0c };
private final String mRRName = "dns.com";
private short mRRType = 0;
private final short mRRClass = CLASS_IN;
private final int mRRTtl = 10;
private int mRdataLen = 0;
private final ArrayList<byte[]> mRdata = new ArrayList<>();
private String mTargetName = null;
private short mSvcPriority = 1;
private boolean mNameCompression = false;
TestDnsRecordByteArrayBuilder setNameCompression(boolean value) {
mNameCompression = value;
return this;
}
TestDnsRecordByteArrayBuilder setRRType(int value) {
mRRType = (short) value;
return this;
}
TestDnsRecordByteArrayBuilder setTargetName(@NonNull String value) throws IOException {
mTargetName = value;
return this;
}
TestDnsRecordByteArrayBuilder setSvcPriority(int value) {
mSvcPriority = (short) value;
return this;
}
TestDnsRecordByteArrayBuilder addRdata(@NonNull byte[] value) {
mRdata.add(value);
mRdataLen += value.length;
return this;
}
byte[] build() throws IOException {
final ByteArrayOutputStream os = new ByteArrayOutputStream();
final byte[] name = mNameCompression ? NAME_COMPRESSION_POINTER
: DnsPacketUtils.DnsRecordParser.domainNameToLabels(mRRName);
os.write(name);
os.write(shortToByteArray(mRRType));
os.write(shortToByteArray(mRRClass));
os.write(HexDump.toByteArray(mRRTtl));
if (mTargetName == null) {
os.write(shortToByteArray((short) mRdataLen));
} else {
final byte[] targetNameLabels =
DnsPacketUtils.DnsRecordParser.domainNameToLabels(mTargetName);
mRdataLen += (Short.BYTES + targetNameLabels.length);
os.write(shortToByteArray((short) mRdataLen));
os.write(shortToByteArray(mSvcPriority));
os.write(targetNameLabels);
}
for (byte[] data : mRdata) {
os.write(data);
}
return os.toByteArray();
}
}
@Test
public void testSliceAndAdvance() throws Exception {
final ByteBuffer buffer = ByteBuffer.wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9});
final ByteBuffer slice1 = DnsSvcbRecord.sliceAndAdvance(buffer, 3);
final ByteBuffer slice2 = DnsSvcbRecord.sliceAndAdvance(buffer, 4);
assertEquals(0, slice1.position());
assertEquals(3, slice1.capacity());
assertEquals(3, slice1.remaining());
assertTrue(slice1.isReadOnly());
assertArrayEquals(new byte[] {1, 2, 3}, getRemainingByteArray(slice1));
assertEquals(0, slice2.position());
assertEquals(4, slice2.capacity());
assertEquals(4, slice2.remaining());
assertTrue(slice2.isReadOnly());
assertArrayEquals(new byte[] {4, 5, 6, 7}, getRemainingByteArray(slice2));
// Nothing is read if out-of-bound access happens.
assertThrows(BufferUnderflowException.class,
() -> DnsSvcbRecord.sliceAndAdvance(buffer, 5));
assertEquals(7, buffer.position());
assertEquals(9, buffer.capacity());
assertEquals(2, buffer.remaining());
assertArrayEquals(new byte[] {8, 9}, getRemainingByteArray(buffer));
}
@Test
public void testDnsSvcbRecord_svcParamMandatory() throws Exception {
final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(TEST_SVC_PARAM_MANDATORY);
// Check the content returned from toString() for now because the getter function for
// this SvcParam hasn't been implemented.
// TODO(b/240259333): Consider adding DnsSvcbRecord.isMandatory(String alpn) when needed.
assertTrue(record.toString().contains("mandatory=ipv4hint,alpn,key333"));
}
@Test
public void testDnsSvcbRecord_svcParamAlpn() throws Exception {
final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(TEST_SVC_PARAM_ALPN_HTTPS);
assertEquals(Arrays.asList("h2", "http/1.1"), record.getAlpns());
}
@Test
public void testDnsSvcbRecord_svcParamNoDefaultAlpn() throws Exception {
final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(
TEST_SVC_PARAM_NO_DEFAULT_ALPN);
// Check the content returned from toString() for now because the getter function for
// this SvcParam hasn't been implemented.
// TODO(b/240259333): Consider adding DnsSvcbRecord.hasNoDefaultAlpn() when needed.
assertTrue(record.toString().contains("no-default-alpn"));
}
@Test
public void testDnsSvcbRecord_svcParamPort() throws Exception {
final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(TEST_SVC_PARAM_PORT);
assertEquals(5353, record.getPort());
}
@Test
public void testDnsSvcbRecord_svcParamIpv4Hint() throws Exception {
final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(TEST_SVC_PARAM_IPV4HINT_2);
assertEquals(Arrays.asList(InetAddresses.parseNumericAddress("4.3.2.1")),
record.getAddresses());
}
@Test
public void testDnsSvcbRecord_svcParamEch() throws Exception {
final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(TEST_SVC_PARAM_ECH);
// Check the content returned from toString() for now because the getter function for
// this SvcParam hasn't been implemented.
// TODO(b/240259333): Consider adding DnsSvcbRecord.getEch() when needed.
assertTrue(record.toString().contains("ech=6142634465"));
}
@Test
public void testDnsSvcbRecord_svcParamIpv6Hint() throws Exception {
final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(TEST_SVC_PARAM_IPV6HINT);
assertEquals(Arrays.asList(InetAddresses.parseNumericAddress("2001:db8::1")),
record.getAddresses());
}
@Test
public void testDnsSvcbRecord_svcParamDohPath() throws Exception {
final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(TEST_SVC_PARAM_DOHPATH);
assertEquals("/some-path{?dns}", record.getDohPath());
}
@Test
public void testDnsSvcbRecord_svcParamGeneric_withValue() throws Exception {
final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(
TEST_SVC_PARAM_GENERIC_WITH_VALUE);
// Check the content returned from toString() for now because the getter function for
// generic SvcParam hasn't been implemented.
// TODO(b/240259333): Consider adding DnsSvcbRecord.getValueFromGenericSvcParam(int key)
// when needed.
assertTrue(record.toString().contains("key12345=1A2B0C"));
}
@Test
public void testDnsSvcbRecord_svcParamGeneric_withoutValue() throws Exception {
final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(
TEST_SVC_PARAM_GENERIC_WITHOUT_VALUE);
// Check the content returned from toString() for now because the getter function for
// generic SvcParam hasn't been implemented.
// TODO(b/240259333): Consider adding DnsSvcbRecord.getValueFromGenericSvcParam(int key)
// when needed.
assertTrue(record.toString().contains("key12346"));
}
@Test
public void testDnsSvcbRecord() throws Exception {
final DnsSvcbRecord record = makeDnsSvcbRecordFromByteArray(
new TestDnsRecordByteArrayBuilder()
.setRRType(TYPE_SVCB)
.setTargetName("doh.dns.com")
.addRdata(TEST_SVC_PARAM_ALPN_HTTPS)
.addRdata(TEST_SVC_PARAM_IPV4HINT_1)
.addRdata(TEST_SVC_PARAM_IPV6HINT)
.addRdata(TEST_SVC_PARAM_PORT)
.addRdata(TEST_SVC_PARAM_DOHPATH)
.build());
assertEquals("doh.dns.com", record.getTargetName());
assertEquals(Arrays.asList("h2", "http/1.1"), record.getAlpns());
assertEquals(5353, record.getPort());
assertEquals(Arrays.asList(
InetAddresses.parseNumericAddress("1.2.3.4"),
InetAddresses.parseNumericAddress("6.7.8.9"),
InetAddresses.parseNumericAddress("2001:db8::1")), record.getAddresses());
assertEquals("/some-path{?dns}", record.getDohPath());
}
@Test
public void testDnsSvcbRecord_createdFromNullObject() throws Exception {
assertThrows(NullPointerException.class, () -> makeDnsSvcbRecordFromByteArray(null));
}
@Test
public void testDnsSvcbRecord_invalidDnsRecord() throws Exception {
// The type is not SVCB.
final byte[] bytes1 = new TestDnsRecordByteArrayBuilder()
.setRRType(TYPE_A)
.addRdata(InetAddresses.parseNumericAddress("1.2.3.4").getAddress())
.build();
assertThrows(IllegalStateException.class, () -> makeDnsSvcbRecordFromByteArray(bytes1));
// TargetName is missing.
final byte[] bytes2 = new TestDnsRecordByteArrayBuilder()
.setRRType(TYPE_SVCB)
.addRdata(new byte[] { 0x01, 0x01 })
.build();
assertThrows(BufferUnderflowException.class, () -> makeDnsSvcbRecordFromByteArray(bytes2));
// Rdata is empty.
final byte[] bytes3 = new TestDnsRecordByteArrayBuilder()
.setRRType(TYPE_SVCB)
.build();
assertThrows(BufferUnderflowException.class, () -> makeDnsSvcbRecordFromByteArray(bytes3));
}
@Test
public void testDnsSvcbRecord_repeatedKeyIsInvalid() throws Exception {
final byte[] bytes = new TestDnsRecordByteArrayBuilder()
.setRRType(TYPE_SVCB)
.addRdata(TEST_SVC_PARAM_ALPN_HTTPS)
.addRdata(TEST_SVC_PARAM_ALPN_DOQ)
.build();
assertThrows(DnsPacket.ParseException.class, () -> makeDnsSvcbRecordFromByteArray(bytes));
}
@Test
public void testDnsSvcbRecord_invalidContent() throws Exception {
final List<byte[]> invalidContents = Arrays.asList(
// Invalid SvcParamValue for "mandatory":
// - SvcParamValue must not be empty.
// - SvcParamValue has less data than expected.
// - SvcParamValue has more data than expected.
// - SvcParamValue must be multiple of 2.
new byte[] { 0x00, 0x00, 0x00, 0x00},
new byte[] { 0x00, 0x00, 0x00, 0x02, 0x00, 0x04, 0x00, 0x06 },
new byte[] { 0x00, 0x00, 0x00, 0x04, 0x00, 0x04, },
new byte[] { 0x00, 0x00, 0x00, 0x03, 0x00, 0x04, 0x00 },
// Invalid SvcParamValue for "alpn":
// - SvcParamValue must not be empty.
// - SvcParamValue has less data than expected.
// - SvcParamValue has more data than expected.
// - Alpn length is less than the actual data size.
// - Alpn length is more than the actual data size.
// - Alpn must be a non-empty string.
new byte[] { 0x00, 0x01, 0x00, 0x00},
new byte[] { 0x00, 0x01, 0x00, 0x02, 0x02, 'h', '2' },
new byte[] { 0x00, 0x01, 0x00, 0x05, 0x02, 'h', '2' },
new byte[] { 0x00, 0x01, 0x00, 0x04, 0x02, 'd', 'o', 't' },
new byte[] { 0x00, 0x01, 0x00, 0x04, 0x08, 'd', 'o', 't' },
new byte[] { 0x00, 0x01, 0x00, 0x08, 0x02, 'h', '2', 0x00 },
// Invalid SvcParamValue for "no-default-alpn":
// - SvcParamValue must be empty.
// - SvcParamValue length must be 0.
new byte[] { 0x00, 0x02, 0x00, 0x04, 'd', 'a', 't', 'a' },
new byte[] { 0x00, 0x02, 0x00, 0x04 },
// Invalid SvcParamValue for "port":
// - SvcParamValue must not be empty.
// - SvcParamValue has less data than expected.
// - SvcParamValue has more data than expected.
// - SvcParamValue length must be multiple of 2.
new byte[] { 0x00, 0x03, 0x00, 0x00 },
new byte[] { 0x00, 0x03, 0x00, 0x02, 0x01 },
new byte[] { 0x00, 0x03, 0x00, 0x02, 0x01, 0x02, 0x03 },
new byte[] { 0x00, 0x03, 0x00, 0x03, 0x01, 0x02, 0x03 },
// Invalid SvcParamValue for "ipv4hint":
// - SvcParamValue must not be empty.
// - SvcParamValue has less data than expected.
// - SvcParamValue has more data than expected.
// - SvcParamValue must be multiple of 4.
new byte[] { 0x00, 0x04, 0x00, 0x00 },
new byte[] { 0x00, 0x04, 0x00, 0x04, 0x08 },
new byte[] { 0x00, 0x04, 0x00, 0x04, 0x08, 0x08, 0x08, 0x08, 0x08 },
new byte[] { 0x00, 0x04, 0x00, 0x05, 0x08, 0x08, 0x08, 0x08 },
// Invalid SvcParamValue for "ipv6hint":
// - SvcParamValue must not be empty.
// - SvcParamValue has less data than expected.
// - SvcParamValue has more data than expected.
// - SvcParamValue must be multiple of 16.
new byte[] { 0x00, 0x06, 0x00, 0x00 },
new byte[] { 0x00, 0x06, 0x00, 0x10, 0x01 },
new byte[] { 0x00, 0x06, 0x00, 0x10, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17 },
new byte[] { 0x00, 0x06, 0x00, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16 }
);
for (byte[] content : invalidContents) {
final byte[] bytes = new TestDnsRecordByteArrayBuilder()
.setRRType(TYPE_SVCB)
.addRdata(content)
.build();
assertThrows(DnsPacket.ParseException.class,
() -> makeDnsSvcbRecordFromByteArray(bytes));
}
}
@Test
public void testDnsSvcbPacket_createdFromNullObject() throws Exception {
assertThrows(DnsPacket.ParseException.class, () -> DnsSvcbPacket.fromResponse(null));
}
@Test
public void testDnsSvcbPacket() throws Exception {
final String dohTargetName = "https.dns.com";
final String doqTargetName = "doq.dns.com";
final InetAddress[] expectedIpAddressesForHttps = new InetAddress[] {
InetAddresses.parseNumericAddress("1.2.3.4"),
InetAddresses.parseNumericAddress("6.7.8.9"),
InetAddresses.parseNumericAddress("2001:db8::1"),
};
final InetAddress[] expectedIpAddressesForDoq = new InetAddress[] {
InetAddresses.parseNumericAddress("4.3.2.1"),
};
final ByteArrayOutputStream os = new ByteArrayOutputStream();
os.write(makeDnsResponseHeaderAsByteArray(1 /* qdcount */, 2 /* ancount */, 0 /* nscount */,
0 /* arcount */));
os.write(TEST_DNS_SVCB_QUESTION_SECTION);
// Add answer for alpn h2 and http/1.1.
os.write(new TestDnsRecordByteArrayBuilder()
.setNameCompression(true)
.setRRType(TYPE_SVCB)
.setTargetName(dohTargetName)
.addRdata(TEST_SVC_PARAM_ALPN_HTTPS)
.addRdata(TEST_SVC_PARAM_IPV4HINT_1)
.addRdata(TEST_SVC_PARAM_IPV6HINT)
.addRdata(TEST_SVC_PARAM_PORT)
.addRdata(TEST_SVC_PARAM_DOHPATH)
.build());
// Add answer for alpn doq.
os.write(new TestDnsRecordByteArrayBuilder()
.setNameCompression(true)
.setRRType(TYPE_SVCB)
.setTargetName(doqTargetName)
.setSvcPriority(2)
.addRdata(TEST_SVC_PARAM_ALPN_DOQ)
.addRdata(TEST_SVC_PARAM_IPV4HINT_2)
.build());
final DnsSvcbPacket pkt = DnsSvcbPacket.fromResponse(os.toByteArray());
assertTrue(pkt.isSupported("http/1.1"));
assertTrue(pkt.isSupported("h2"));
assertTrue(pkt.isSupported("doq"));
assertFalse(pkt.isSupported("http"));
assertFalse(pkt.isSupported("h3"));
assertFalse(pkt.isSupported(""));
assertEquals(dohTargetName, pkt.getTargetName("http/1.1"));
assertEquals(dohTargetName, pkt.getTargetName("h2"));
assertEquals(doqTargetName, pkt.getTargetName("doq"));
assertEquals(null, pkt.getTargetName("http"));
assertEquals(null, pkt.getTargetName("h3"));
assertEquals(null, pkt.getTargetName(""));
assertEquals(5353, pkt.getPort("http/1.1"));
assertEquals(5353, pkt.getPort("h2"));
assertEquals(-1, pkt.getPort("doq"));
assertEquals(-1, pkt.getPort("http"));
assertEquals(-1, pkt.getPort("h3"));
assertEquals(-1, pkt.getPort(""));
assertArrayEquals(expectedIpAddressesForHttps, pkt.getAddresses("http/1.1").toArray());
assertArrayEquals(expectedIpAddressesForHttps, pkt.getAddresses("h2").toArray());
assertArrayEquals(expectedIpAddressesForDoq, pkt.getAddresses("doq").toArray());
assertTrue(pkt.getAddresses("http").isEmpty());
assertTrue(pkt.getAddresses("h3").isEmpty());
assertTrue(pkt.getAddresses("").isEmpty());
assertEquals("/some-path{?dns}", pkt.getDohPath("http/1.1"));
assertEquals("/some-path{?dns}", pkt.getDohPath("h2"));
assertEquals("", pkt.getDohPath("doq"));
assertEquals(null, pkt.getDohPath("http"));
assertEquals(null, pkt.getDohPath("h3"));
assertEquals(null, pkt.getDohPath(""));
}
@Test
public void testDnsSvcbPacket_noIpHint() throws Exception {
final String targetName = "doq.dns.com";
final ByteArrayOutputStream os = new ByteArrayOutputStream();
os.write(makeDnsResponseHeaderAsByteArray(1 /* qdcount */, 1 /* ancount */, 0 /* nscount */,
0 /* arcount */));
os.write(TEST_DNS_SVCB_QUESTION_SECTION);
// Add answer for alpn doq.
os.write(new TestDnsRecordByteArrayBuilder()
.setNameCompression(true)
.setRRType(TYPE_SVCB)
.setTargetName(targetName)
.addRdata(TEST_SVC_PARAM_ALPN_DOQ)
.build());
final DnsSvcbPacket pkt = DnsSvcbPacket.fromResponse(os.toByteArray());
assertTrue(pkt.isSupported("doq"));
assertEquals(targetName, pkt.getTargetName("doq"));
assertEquals(-1, pkt.getPort("doq"));
assertArrayEquals(new InetAddress[] {}, pkt.getAddresses("doq").toArray());
assertEquals("", pkt.getDohPath("doq"));
}
@Test
public void testDnsSvcbPacket_hasAnswerInAdditionalSection() throws Exception {
final InetAddress[] expectedIpAddresses = new InetAddress[] {
InetAddresses.parseNumericAddress("1.2.3.4"),
InetAddresses.parseNumericAddress("2001:db8::2"),
};
final ByteArrayOutputStream os = new ByteArrayOutputStream();
os.write(makeDnsResponseHeaderAsByteArray(1 /* qdcount */, 1 /* ancount */, 0 /* nscount */,
2 /* arcount */));
os.write(TEST_DNS_SVCB_QUESTION_SECTION);
// Add SVCB record in the Answer section.
os.write(new TestDnsRecordByteArrayBuilder()
.setNameCompression(true)
.setRRType(TYPE_SVCB)
.setTargetName("doq.dns.com")
.addRdata(TEST_SVC_PARAM_ALPN_DOQ)
.addRdata(TEST_SVC_PARAM_IPV4HINT_2)
.addRdata(TEST_SVC_PARAM_IPV6HINT)
.build());
// Add A/AAAA records in the Additional section.
os.write(new TestDnsRecordByteArrayBuilder()
.setNameCompression(true)
.setRRType(TYPE_A)
.addRdata(InetAddresses.parseNumericAddress("1.2.3.4").getAddress())
.build());
os.write(new TestDnsRecordByteArrayBuilder()
.setNameCompression(true)
.setRRType(TYPE_AAAA)
.addRdata(InetAddresses.parseNumericAddress("2001:db8::2").getAddress())
.build());
final DnsSvcbPacket pkt = DnsSvcbPacket.fromResponse(os.toByteArray());
// If there are A/AAAA records in the Additional section, getAddresses() returns the IP
// addresses in those records instead of the IP addresses in ipv4hint/ipv6hint.
assertArrayEquals(expectedIpAddresses, pkt.getAddresses("doq").toArray());
}
}