blob: 701afd5ac4ac81496f8c90ef988bd9a5faa66888 [file] [log] [blame]
/*
* Copyright (C) 2021 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.google.android.iwlan.epdg;
import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.net.DnsResolver;
import android.net.DnsResolver.DnsException;
import android.net.Network;
import android.net.ParseException;
import android.os.CancellationSignal;
import android.util.Log;
import com.android.net.module.util.DnsPacket;
import com.android.net.module.util.DnsPacketUtils.DnsRecordParser;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
/**
* A utility wrapper around android.net.DnsResolver that queries for NAPTR DNS Resource Records, and
* returns in the user callback a list of server (IP addresses, port number) combinations pertaining
* to the service requested.
*/
final class NaptrDnsResolver {
private static final String TAG = "NaptrDnsResolver";
@IntDef(
prefix = {"TYPE_"},
value = {TYPE_A, TYPE_SRV, TYPE_U, TYPE_P})
@Retention(RetentionPolicy.SOURCE)
@interface NaptrRecordType {}
public static final int TYPE_A = 0;
public static final int TYPE_SRV = 1;
// These below record types are not currently supported.
public static final int TYPE_U = 2;
public static final int TYPE_P = 3;
/**
* A NAPTR record comprises of a target domain name, along with the type of the record, as
* defined in RFC 2915.
*/
static class NaptrTarget {
public final String mName;
public final int mType;
public NaptrTarget(String name, @NaptrRecordType int type) {
mName = name;
mType = type;
}
}
static final int QUERY_TYPE_NAPTR = 35;
static class NaptrResponse extends DnsPacket {
/*
* Parses and stores a NAPTR record as described in RFC 2915.
*/
static class NaptrRecord {
// A 16-bit unsigned value- the client should prioritize records with lower
// 'preference'.
public final int preference;
// A 16-bit unsigned value- for records with the same 'preference' field, the client
// should prioritize records with lower 'order'.
public final int order;
// A string that denotes the @NaptrRecordType.
@NonNull public final String flag;
// A free form string that denotes the service provided by the server described by the
// record- SIP, email, etc.
public final String service;
// A string that describes the regex transformation (described in RFC 2915) that needs
// to be applied to the DNS query domain name for further processes. RFC 2915 describes
// that in a NaptrRecord, exactly one of |regex| and |replacement| must be non-null.
@Nullable public final String regex;
// This string describes how the input DNS query domain name should be replaced. With
// the 'flag' and 'service' field, this instructs the DNS client on what to do next.
// For current use cases, only the |replacement| field is expected to be non-null in a
// NaptrRecord.
@Nullable public final String replacement;
private static final int MAXNAMESIZE = 255;
private String parseNextField(ByteBuffer buf) throws BufferUnderflowException {
final short size = buf.get();
// size can also be 0, for instance for the 'regex' field.
final byte[] field = new byte[size];
buf.get(field, 0, size);
return new String(field, StandardCharsets.UTF_8);
}
@NaptrRecordType
public int getTypeFromFlagString() {
switch (flag) {
case "S":
case "s":
return TYPE_SRV;
case "A":
case "a":
return TYPE_A;
default:
throw new ParseException("Unsupported flag type: " + flag);
}
}
NaptrRecord(byte[] naptrRecordData) throws ParseException {
final ByteBuffer buf = ByteBuffer.wrap(naptrRecordData);
try {
order = Short.toUnsignedInt(buf.getShort());
preference = Short.toUnsignedInt(buf.getShort());
flag = parseNextField(buf);
service = parseNextField(buf);
regex = parseNextField(buf);
if (regex.length() != 0) {
throw new ParseException("NAPTR: regex field expected to be empty!");
}
replacement =
DnsRecordParser.parseName(
buf, 0, /* isNameCompressionSupported */ true);
if (replacement == null) {
throw new ParseException(
"NAPTR: replacement field not expected to be empty!");
}
if (replacement.length() > MAXNAMESIZE) {
throw new ParseException(
"Parse name fail, replacement name size is too long: "
+ replacement.length());
}
if (buf.hasRemaining()) {
throw new ParseException(
"Parsing NAPTR record data failed: more bytes than expected!");
}
} catch (BufferUnderflowException e) {
throw new ParseException("Parsing NAPTR Record data failed with cause", e);
}
}
}
private final int mQueryType;
NaptrResponse(@NonNull byte[] data) throws ParseException {
super(data);
if (!mHeader.isResponse()) {
throw new ParseException("Not an answer packet");
}
int numQueries = mHeader.getRecordCount(QDSECTION);
// Expects exactly one query in query section.
if (numQueries != 1) {
throw new ParseException("Unexpected query count: " + numQueries);
}
// Expect only one question in question section.
mQueryType = mRecords[QDSECTION].get(0).nsType;
if (mQueryType != QUERY_TYPE_NAPTR) {
throw new ParseException("Unexpected query type: " + mQueryType);
}
}
public @NonNull List<NaptrRecord> parseNaptrRecords() throws ParseException {
final List<NaptrRecord> naptrRecords = new ArrayList<>();
if (mHeader.getRecordCount(ANSECTION) == 0) return naptrRecords;
for (final DnsRecord ansSec : mRecords[ANSECTION]) {
final int nsType = ansSec.nsType;
if (nsType != QUERY_TYPE_NAPTR) {
throw new ParseException("Unexpected DNS record type in ANSECTION: " + nsType);
}
final NaptrRecord record = new NaptrRecord(ansSec.getRR());
naptrRecords.add(record);
Log.d(
TAG,
"NaptrRecord name: "
+ ansSec.dName
+ " replacement field: "
+ record.replacement);
}
return naptrRecords;
}
}
/**
* A decorator for DnsResolver.Callback that accumulates IPv4/v6 responses for NAPTR DNS queries
* and passes it up to the user callback.
*/
static class NaptrRecordAnswerAccumulator implements DnsResolver.Callback<byte[]> {
private static final String TAG = "NaptrRecordAnswerAccum";
private final DnsResolver.Callback<List<NaptrTarget>> mUserCallback;
private final Executor mUserExecutor;
private static class LazyExecutor {
public static final Executor INSTANCE = Executors.newSingleThreadExecutor();
}
static Executor getInternalExecutor() {
return LazyExecutor.INSTANCE;
}
NaptrRecordAnswerAccumulator(
@NonNull DnsResolver.Callback<List<NaptrTarget>> callback,
@NonNull @CallbackExecutor Executor executor) {
mUserCallback = callback;
mUserExecutor = executor;
}
private List<NaptrTarget> composeNaptrRecordResult(
List<NaptrResponse.NaptrRecord> responses) throws ParseException {
final List<NaptrTarget> records = new ArrayList<>();
if (responses.isEmpty()) return records;
for (NaptrResponse.NaptrRecord response : responses) {
records.add(
new NaptrTarget(response.replacement, response.getTypeFromFlagString()));
}
return records;
}
@Override
public void onAnswer(@NonNull byte[] answer, int rcode) {
try {
final NaptrResponse response = new NaptrResponse(answer);
final List<NaptrTarget> result =
composeNaptrRecordResult(response.parseNaptrRecords());
mUserExecutor.execute(() -> mUserCallback.onAnswer(result, rcode));
} catch (DnsPacket.ParseException e) {
// Convert the com.android.net.module.util.DnsPacket.ParseException to an
// android.net.ParseException. This is the type that was used in Q and is implied
// by the public documentation of ERROR_PARSE.
//
// DnsPacket cannot throw android.net.ParseException directly because it's @hide.
final ParseException pe = new ParseException(e.reason, e.getCause());
pe.setStackTrace(e.getStackTrace());
Log.e(TAG, "ParseException", pe);
mUserExecutor.execute(
() -> mUserCallback.onError(new DnsException(DnsResolver.ERROR_PARSE, pe)));
}
}
@Override
public void onError(@NonNull DnsException error) {
Log.e(TAG, "onError: " + error);
mUserExecutor.execute(() -> mUserCallback.onError(error));
}
}
/**
* Send an NAPTR DNS query on the specified network. The answer will be provided asynchronously
* on the passed executor, through the provided {@link DnsResolver.Callback}.
*
* @param network {@link Network} specifying which network to query on. {@code null} for query
* on default network.
* @param domain NAPTR domain name to query.
* @param cancellationSignal used by the caller to signal if the query should be cancelled. May
* be {@code null}.
* @param callback a {@link DnsResolver.Callback} which will be called on a separate thread to
* notify the caller of the result of dns query.
*/
public static void query(
@Nullable Network network,
@NonNull String domain,
@NonNull @CallbackExecutor Executor executor,
@Nullable CancellationSignal cancellationSignal,
@NonNull DnsResolver.Callback<List<NaptrTarget>> callback) {
final NaptrRecordAnswerAccumulator naptrDnsCb =
new NaptrRecordAnswerAccumulator(callback, executor);
DnsResolver.getInstance()
.rawQuery(
network,
domain,
DnsResolver.CLASS_IN,
QUERY_TYPE_NAPTR,
DnsResolver.FLAG_EMPTY,
NaptrRecordAnswerAccumulator.getInternalExecutor(),
cancellationSignal,
naptrDnsCb);
}
private NaptrDnsResolver() {}
}