Merge changes from topic "dns_svcb" into main
* changes: Add tests for DnsSvcbRecord and DnsSvcbPacket Add DnsSvcbPacket Add DnsSvcbRecord
This commit is contained in:
commit
d97fdbb3bc
6 changed files with 1337 additions and 1 deletions
|
|
@ -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",
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue