diff options
| author | Luke Huang <huangluke@google.com> | 2019-01-23 01:14:34 -0800 |
|---|---|---|
| committer | android-build-merger <android-build-merger@google.com> | 2019-01-23 01:14:34 -0800 |
| commit | e052154abb25cff4d657ca98b5591b5a49bab8cf (patch) | |
| tree | c6aba0ef33c4623a69290dbdf6aa04918cac86ba /core/java/android | |
| parent | 716eb586ac7ac40e52e616cec9d790475cc5ee89 (diff) | |
| parent | a8e16262f18ab839bb443fb140286f4e59ef7020 (diff) | |
Merge changes from topic "Java async dns" am: 31ff0616d1
am: a8e16262f1
Change-Id: I305cf0ae791f9503519884bdf382863002357ac7
Diffstat (limited to 'core/java/android')
| -rw-r--r-- | core/java/android/net/DnsPacket.java | 235 | ||||
| -rw-r--r-- | core/java/android/net/DnsResolver.java | 289 | ||||
| -rw-r--r-- | core/java/android/net/NetworkUtils.java | 28 |
3 files changed, 552 insertions, 0 deletions
diff --git a/core/java/android/net/DnsPacket.java b/core/java/android/net/DnsPacket.java new file mode 100644 index 000000000000..458fb340b196 --- /dev/null +++ b/core/java/android/net/DnsPacket.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2019 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 android.net; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.text.TextUtils; + +import com.android.internal.util.BitUtils; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.text.DecimalFormat; +import java.text.FieldPosition; +import java.util.ArrayList; +import java.util.List; +import java.util.StringJoiner; + +/** + * Defines basic data for DNS protocol based on RFC 1035. + * Subclasses create the specific format used in DNS packet. + * + * @hide + */ +public abstract class DnsPacket { + public class DnsHeader { + private static final String TAG = "DnsHeader"; + public final int id; + public final int flags; + public final int rcode; + private final int[] mSectionCount; + + /** + * Create a new DnsHeader from a positioned ByteBuffer. + * + * The ByteBuffer must be in network byte order (which is the default). + * Reads the passed ByteBuffer from its current position and decodes a DNS header. + * When this constructor returns, the reading position of the ByteBuffer has been + * advanced to the end of the DNS header record. + * This is meant to chain with other methods reading a DNS response in sequence. + * + */ + DnsHeader(@NonNull ByteBuffer buf) throws BufferUnderflowException { + id = BitUtils.uint16(buf.getShort()); + flags = BitUtils.uint16(buf.getShort()); + rcode = flags & 0xF; + mSectionCount = new int[NUM_SECTIONS]; + for (int i = 0; i < NUM_SECTIONS; ++i) { + mSectionCount[i] = BitUtils.uint16(buf.getShort()); + } + } + + /** + * Get section count by section type. + */ + public int getSectionCount(int sectionType) { + return mSectionCount[sectionType]; + } + } + + public class DnsSection { + private static final int MAXNAMESIZE = 255; + private static final int MAXLABELSIZE = 63; + private static final int MAXLABELCOUNT = 128; + private static final int NAME_NORMAL = 0; + private static final int NAME_COMPRESSION = 0xC0; + private final DecimalFormat byteFormat = new DecimalFormat(); + private final FieldPosition pos = new FieldPosition(0); + + private static final String TAG = "DnsSection"; + + public final String dName; + public final int nsType; + public final int nsClass; + public final long ttl; + private final byte[] mRR; + + /** + * Create a new DnsSection from a positioned ByteBuffer. + * + * The ByteBuffer must be in network byte order (which is the default). + * Reads the passed ByteBuffer from its current position and decodes a DNS section. + * When this constructor returns, the reading position of the ByteBuffer has been + * advanced to the end of the DNS header record. + * This is meant to chain with other methods reading a DNS response in sequence. + * + */ + DnsSection(int sectionType, @NonNull ByteBuffer buf) + throws BufferUnderflowException, ParseException { + dName = parseName(buf, 0 /* Parse depth */); + if (dName.length() > MAXNAMESIZE) { + throw new ParseException("Parse name fail, name size is too long"); + } + nsType = BitUtils.uint16(buf.getShort()); + nsClass = BitUtils.uint16(buf.getShort()); + + if (sectionType != QDSECTION) { + ttl = BitUtils.uint32(buf.getInt()); + final int length = BitUtils.uint16(buf.getShort()); + mRR = new byte[length]; + buf.get(mRR); + } else { + ttl = 0; + mRR = null; + } + } + + /** + * Get a copy of rr. + */ + @Nullable public byte[] getRR() { + return (mRR == null) ? null : mRR.clone(); + } + + /** + * Convert label from {@code byte[]} to {@code String} + * + * It follows the same converting rule as native layer. + * (See ns_name.c in libc) + * + */ + private String labelToString(@NonNull byte[] label) { + final StringBuffer sb = new StringBuffer(); + for (int i = 0; i < label.length; ++i) { + int b = BitUtils.uint8(label[i]); + // Control characters and non-ASCII characters. + if (b <= 0x20 || b >= 0x7f) { + sb.append('\\'); + byteFormat.format(b, sb, pos); + } else if (b == '"' || b == '.' || b == ';' || b == '\\' + || b == '(' || b == ')' || b == '@' || b == '$') { + sb.append('\\'); + sb.append((char) b); + } else { + sb.append((char) b); + } + } + return sb.toString(); + } + + private String parseName(@NonNull ByteBuffer buf, int depth) throws + BufferUnderflowException, ParseException { + if (depth > MAXLABELCOUNT) throw new ParseException("Parse name fails, too many labels"); + final int len = BitUtils.uint8(buf.get()); + final int mask = len & NAME_COMPRESSION; + if (0 == len) { + return ""; + } else if (mask != NAME_NORMAL && mask != NAME_COMPRESSION) { + throw new ParseException("Parse name fail, bad label type"); + } else if (mask == NAME_COMPRESSION) { + // Name compression based on RFC 1035 - 4.1.4 Message compression + final int offset = ((len & ~NAME_COMPRESSION) << 8) + BitUtils.uint8(buf.get()); + final int oldPos = buf.position(); + if (offset >= oldPos - 2) { + throw new ParseException("Parse compression name fail, invalid compression"); + } + buf.position(offset); + final String pointed = parseName(buf, depth + 1); + buf.position(oldPos); + return pointed; + } else { + final byte[] label = new byte[len]; + buf.get(label); + final String head = labelToString(label); + if (head.length() > MAXLABELSIZE) { + throw new ParseException("Parse name fail, invalid label length"); + } + final String tail = parseName(buf, depth + 1); + return TextUtils.isEmpty(tail) ? head : head + "." + tail; + } + } + } + + public static final int QDSECTION = 0; + public static final int ANSECTION = 1; + public static final int NSSECTION = 2; + public static final int ARSECTION = 3; + private static final int NUM_SECTIONS = ARSECTION + 1; + + private static final String TAG = DnsPacket.class.getSimpleName(); + + protected final DnsHeader mHeader; + protected final List<DnsSection>[] mSections; + + public static class ParseException extends Exception { + public ParseException(String msg) { + super(msg); + } + + public ParseException(String msg, Throwable cause) { + super(msg, cause); + } + } + + protected DnsPacket(@NonNull byte[] data) throws ParseException { + if (null == data) throw new ParseException("Parse header failed, null input data"); + final ByteBuffer buffer; + try { + buffer = ByteBuffer.wrap(data); + mHeader = new DnsHeader(buffer); + } catch (BufferUnderflowException e) { + throw new ParseException("Parse Header fail, bad input data", e); + } + + mSections = new ArrayList[NUM_SECTIONS]; + + for (int i = 0; i < NUM_SECTIONS; ++i) { + final int count = mHeader.getSectionCount(i); + if (count > 0) { + mSections[i] = new ArrayList(count); + } + for (int j = 0; j < count; ++j) { + try { + mSections[i].add(new DnsSection(i, buffer)); + } catch (BufferUnderflowException e) { + throw new ParseException("Parse section fail", e); + } + } + } + } +} diff --git a/core/java/android/net/DnsResolver.java b/core/java/android/net/DnsResolver.java new file mode 100644 index 000000000000..6d54264cd89f --- /dev/null +++ b/core/java/android/net/DnsResolver.java @@ -0,0 +1,289 @@ +/* + * Copyright (C) 2019 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 android.net; + +import static android.net.NetworkUtils.resNetworkQuery; +import static android.net.NetworkUtils.resNetworkResult; +import static android.net.NetworkUtils.resNetworkSend; +import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR; +import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Handler; +import android.os.MessageQueue; +import android.system.ErrnoException; +import android.util.Log; + +import java.io.FileDescriptor; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + + +/** + * Dns resolver class for asynchronous dns querying + * + */ +public final class DnsResolver { + private static final String TAG = "DnsResolver"; + private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR; + private static final int MAXPACKET = 8 * 1024; + + @IntDef(prefix = { "CLASS_" }, value = { + CLASS_IN + }) + @Retention(RetentionPolicy.SOURCE) + @interface QueryClass {} + public static final int CLASS_IN = 1; + + @IntDef(prefix = { "TYPE_" }, value = { + TYPE_A, + TYPE_AAAA + }) + @Retention(RetentionPolicy.SOURCE) + @interface QueryType {} + public static final int TYPE_A = 1; + public static final int TYPE_AAAA = 28; + + @IntDef(prefix = { "FLAG_" }, value = { + FLAG_EMPTY, + FLAG_NO_RETRY, + FLAG_NO_CACHE_STORE, + FLAG_NO_CACHE_LOOKUP + }) + @Retention(RetentionPolicy.SOURCE) + @interface QueryFlag {} + public static final int FLAG_EMPTY = 0; + public static final int FLAG_NO_RETRY = 1 << 0; + public static final int FLAG_NO_CACHE_STORE = 1 << 1; + public static final int FLAG_NO_CACHE_LOOKUP = 1 << 2; + + private static final int DNS_RAW_RESPONSE = 1; + + private static final int NETID_UNSET = 0; + + private static final DnsResolver sInstance = new DnsResolver(); + + /** + * listener for receiving raw answers + */ + public interface RawAnswerListener { + /** + * {@code byte[]} is {@code null} if query timed out + */ + void onAnswer(@Nullable byte[] answer); + } + + /** + * listener for receiving parsed answers + */ + public interface InetAddressAnswerListener { + /** + * Will be called exactly once with all the answers to the query. + * size of addresses will be zero if no available answer could be parsed. + */ + void onAnswer(@NonNull List<InetAddress> addresses); + } + + /** + * Get instance for DnsResolver + */ + public static DnsResolver getInstance() { + return sInstance; + } + + private DnsResolver() {} + + /** + * Pass in a blob and corresponding setting, + * get a blob back asynchronously with the entire raw answer. + * + * @param network {@link Network} specifying which network for querying. + * {@code null} for query on default network. + * @param query blob message + * @param flags flags as a combination of the FLAGS_* constants + * @param handler {@link Handler} to specify the thread + * upon which the {@link RawAnswerListener} will be invoked. + * @param listener a {@link RawAnswerListener} which will be called to notify the caller + * of the result of dns query. + */ + public void query(@Nullable Network network, @NonNull byte[] query, @QueryFlag int flags, + @NonNull Handler handler, @NonNull RawAnswerListener listener) throws ErrnoException { + final FileDescriptor queryfd = resNetworkSend((network != null + ? network.netId : NETID_UNSET), query, query.length, flags); + registerFDListener(handler.getLooper().getQueue(), queryfd, + answerbuf -> listener.onAnswer(answerbuf)); + } + + /** + * Pass in a domain name and corresponding setting, + * get a blob back asynchronously with the entire raw answer. + * + * @param network {@link Network} specifying which network for querying. + * {@code null} for query on default network. + * @param domain domain name for querying + * @param nsClass dns class as one of the CLASS_* constants + * @param nsType dns resource record (RR) type as one of the TYPE_* constants + * @param flags flags as a combination of the FLAGS_* constants + * @param handler {@link Handler} to specify the thread + * upon which the {@link RawAnswerListener} will be invoked. + * @param listener a {@link RawAnswerListener} which will be called to notify the caller + * of the result of dns query. + */ + public void query(@Nullable Network network, @NonNull String domain, @QueryClass int nsClass, + @QueryType int nsType, @QueryFlag int flags, + @NonNull Handler handler, @NonNull RawAnswerListener listener) throws ErrnoException { + final FileDescriptor queryfd = resNetworkQuery((network != null + ? network.netId : NETID_UNSET), domain, nsClass, nsType, flags); + registerFDListener(handler.getLooper().getQueue(), queryfd, + answerbuf -> listener.onAnswer(answerbuf)); + } + + /** + * Pass in a domain name and corresponding setting, + * get back a set of InetAddresses asynchronously. + * + * @param network {@link Network} specifying which network for querying. + * {@code null} for query on default network. + * @param domain domain name for querying + * @param flags flags as a combination of the FLAGS_* constants + * @param handler {@link Handler} to specify the thread + * upon which the {@link InetAddressAnswerListener} will be invoked. + * @param listener an {@link InetAddressAnswerListener} which will be called to + * notify the caller of the result of dns query. + * + */ + public void query(@Nullable Network network, @NonNull String domain, @QueryFlag int flags, + @NonNull Handler handler, @NonNull InetAddressAnswerListener listener) + throws ErrnoException { + final FileDescriptor v4fd = resNetworkQuery((network != null + ? network.netId : NETID_UNSET), domain, CLASS_IN, TYPE_A, flags); + final FileDescriptor v6fd = resNetworkQuery((network != null + ? network.netId : NETID_UNSET), domain, CLASS_IN, TYPE_AAAA, flags); + + final InetAddressAnswerAccumulator accmulator = + new InetAddressAnswerAccumulator(2, listener); + final Consumer<byte[]> consumer = answerbuf -> + accmulator.accumulate(parseAnswers(answerbuf)); + + registerFDListener(handler.getLooper().getQueue(), v4fd, consumer); + registerFDListener(handler.getLooper().getQueue(), v6fd, consumer); + } + + private void registerFDListener(@NonNull MessageQueue queue, + @NonNull FileDescriptor queryfd, @NonNull Consumer<byte[]> answerConsumer) { + queue.addOnFileDescriptorEventListener( + queryfd, + FD_EVENTS, + (fd, events) -> { + byte[] answerbuf = null; + try { + // TODO: Implement result function in Java side instead of using JNI + // Because JNI method close fd prior than unregistering fd on + // event listener. + answerbuf = resNetworkResult(fd); + } catch (ErrnoException e) { + Log.e(TAG, "resNetworkResult:" + e.toString()); + } + answerConsumer.accept(answerbuf); + + // Unregister this fd listener + return 0; + }); + } + + private class DnsAddressAnswer extends DnsPacket { + private static final String TAG = "DnsResolver.DnsAddressAnswer"; + private static final boolean DBG = false; + + private final int mQueryType; + + DnsAddressAnswer(@NonNull byte[] data) throws ParseException { + super(data); + if ((mHeader.flags & (1 << 15)) == 0) { + throw new ParseException("Not an answer packet"); + } + if (mHeader.rcode != 0) { + throw new ParseException("Response error, rcode:" + mHeader.rcode); + } + if (mHeader.getSectionCount(ANSECTION) == 0) { + throw new ParseException("No available answer"); + } + if (mHeader.getSectionCount(QDSECTION) == 0) { + throw new ParseException("No question found"); + } + // Assume only one question per answer packet. (RFC1035) + mQueryType = mSections[QDSECTION].get(0).nsType; + } + + public @NonNull List<InetAddress> getAddresses() { + final List<InetAddress> results = new ArrayList<InetAddress>(); + for (final DnsSection ansSec : mSections[ANSECTION]) { + // Only support A and AAAA, also ignore answers if query type != answer type. + int nsType = ansSec.nsType; + if (nsType != mQueryType || (nsType != TYPE_A && nsType != TYPE_AAAA)) { + continue; + } + try { + results.add(InetAddress.getByAddress(ansSec.getRR())); + } catch (UnknownHostException e) { + if (DBG) { + Log.w(TAG, "rr to address fail"); + } + } + } + return results; + } + } + + private @Nullable List<InetAddress> parseAnswers(@Nullable byte[] data) { + try { + return (data == null) ? null : new DnsAddressAnswer(data).getAddresses(); + } catch (DnsPacket.ParseException e) { + Log.e(TAG, "Parse answer fail " + e.getMessage()); + return null; + } + } + + private class InetAddressAnswerAccumulator { + private final List<InetAddress> mAllAnswers; + private final InetAddressAnswerListener mAnswerListener; + private final int mTargetAnswerCount; + private int mReceivedAnswerCount = 0; + + InetAddressAnswerAccumulator(int size, @NonNull InetAddressAnswerListener listener) { + mTargetAnswerCount = size; + mAllAnswers = new ArrayList<>(); + mAnswerListener = listener; + } + + public void accumulate(@Nullable List<InetAddress> answer) { + if (null != answer) { + mAllAnswers.addAll(answer); + } + if (++mReceivedAnswerCount == mTargetAnswerCount) { + mAnswerListener.onAnswer(mAllAnswers); + } + } + } +} diff --git a/core/java/android/net/NetworkUtils.java b/core/java/android/net/NetworkUtils.java index c0aa4a6faf12..7f4d8cd1cfcb 100644 --- a/core/java/android/net/NetworkUtils.java +++ b/core/java/android/net/NetworkUtils.java @@ -34,6 +34,8 @@ import java.util.Collection; import java.util.Locale; import java.util.TreeSet; +import android.system.ErrnoException; + /** * Native methods for managing network interfaces. * @@ -133,6 +135,32 @@ public class NetworkUtils { public native static boolean queryUserAccess(int uid, int netId); /** + * DNS resolver series jni method. + * Issue the query {@code msg} on the network designated by {@code netId}. + * {@code flags} is an additional config to control actual querying behavior. + * @return a file descriptor to watch for read events + */ + public static native FileDescriptor resNetworkSend( + int netId, byte[] msg, int msglen, int flags) throws ErrnoException; + + /** + * DNS resolver series jni method. + * Look up the {@code nsClass} {@code nsType} Resource Record (RR) associated + * with Domain Name {@code dname} on the network designated by {@code netId}. + * {@code flags} is an additional config to control actual querying behavior. + * @return a file descriptor to watch for read events + */ + public static native FileDescriptor resNetworkQuery( + int netId, String dname, int nsClass, int nsType, int flags) throws ErrnoException; + + /** + * DNS resolver series jni method. + * Read a result for the query associated with the {@code fd}. + * @return a byte array containing blob answer + */ + public static native byte[] resNetworkResult(FileDescriptor fd) throws ErrnoException; + + /** * Add an entry into the ARP cache. */ public static void addArpEntry(Inet4Address ipv4Addr, MacAddress ethAddr, String ifname, |
