diff options
| author | 2019-01-23 01:06:19 -0800 | |
|---|---|---|
| committer | 2019-01-23 01:06:19 -0800 | |
| commit | a8e16262f18ab839bb443fb140286f4e59ef7020 (patch) | |
| tree | 2b31e1b2c595319f8e1072f7833832da811de612 | |
| parent | 2f1cba20a5b63476e4bcbe0609a21c37a2eb0380 (diff) | |
| parent | 31ff0616d1a940f35d25f9c0fb24c9d3a1b9d6d6 (diff) | |
Merge changes from topic "Java async dns"
am: 31ff0616d1
Change-Id: Ia08a8300887315d673f398b1340b2f44ce501b14
| -rwxr-xr-x | api/current.txt | 22 | ||||
| -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 | ||||
| -rw-r--r-- | core/jni/android_net_NetUtils.cpp | 88 | ||||
| -rw-r--r-- | tests/net/java/android/net/DnsPacketTest.java | 159 |
6 files changed, 821 insertions, 0 deletions
diff --git a/api/current.txt b/api/current.txt index 7ea15a5c014f..233568a03fd4 100755 --- a/api/current.txt +++ b/api/current.txt @@ -27212,6 +27212,28 @@ package android.net { field public int serverAddress; } + public final class DnsResolver { + method public static android.net.DnsResolver getInstance(); + method public void query(@Nullable android.net.Network, @NonNull byte[], int, @NonNull android.os.Handler, @NonNull android.net.DnsResolver.RawAnswerListener) throws android.system.ErrnoException; + method public void query(@Nullable android.net.Network, @NonNull String, int, int, int, @NonNull android.os.Handler, @NonNull android.net.DnsResolver.RawAnswerListener) throws android.system.ErrnoException; + method public void query(@Nullable android.net.Network, @NonNull String, int, @NonNull android.os.Handler, @NonNull android.net.DnsResolver.InetAddressAnswerListener) throws android.system.ErrnoException; + field public static final int CLASS_IN = 1; // 0x1 + field public static final int FLAG_EMPTY = 0; // 0x0 + field public static final int FLAG_NO_CACHE_LOOKUP = 4; // 0x4 + field public static final int FLAG_NO_CACHE_STORE = 2; // 0x2 + field public static final int FLAG_NO_RETRY = 1; // 0x1 + field public static final int TYPE_A = 1; // 0x1 + field public static final int TYPE_AAAA = 28; // 0x1c + } + + public static interface DnsResolver.InetAddressAnswerListener { + method public void onAnswer(@NonNull java.util.List<java.net.InetAddress>); + } + + public static interface DnsResolver.RawAnswerListener { + method public void onAnswer(@Nullable byte[]); + } + public class InetAddresses { method public static boolean isNumericAddress(String); method public static java.net.InetAddress parseNumericAddress(String); 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, diff --git a/core/jni/android_net_NetUtils.cpp b/core/jni/android_net_NetUtils.cpp index 9b138ebb760a..7eddcfe425d3 100644 --- a/core/jni/android_net_NetUtils.cpp +++ b/core/jni/android_net_NetUtils.cpp @@ -16,8 +16,11 @@ #define LOG_TAG "NetUtils" +#include <vector> + #include "jni.h" #include <nativehelper/JNIHelp.h> +#include <nativehelper/ScopedLocalRef.h> #include "NetdClient.h" #include <utils/misc.h> #include <android_runtime/AndroidRuntime.h> @@ -55,6 +58,31 @@ static const uint32_t kUDPSrcPortIndirectOffset = kEtherHeaderLen + offsetof(udp static const uint32_t kUDPDstPortIndirectOffset = kEtherHeaderLen + offsetof(udphdr, dest); static const uint16_t kDhcpClientPort = 68; +constexpr int MAXPACKETSIZE = 8 * 1024; +// FrameworkListener limits the size of commands to 1024 bytes. TODO: fix this. +constexpr int MAXCMDSIZE = 1024; + +static void throwErrnoException(JNIEnv* env, const char* functionName, int error) { + ScopedLocalRef<jstring> detailMessage(env, env->NewStringUTF(functionName)); + if (detailMessage.get() == NULL) { + // Not really much we can do here. We're probably dead in the water, + // but let's try to stumble on... + env->ExceptionClear(); + } + static jclass errnoExceptionClass = + MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/system/ErrnoException")); + + static jmethodID errnoExceptionCtor = + GetMethodIDOrDie(env, errnoExceptionClass, + "<init>", "(Ljava/lang/String;I)V"); + + jobject exception = env->NewObject(errnoExceptionClass, + errnoExceptionCtor, + detailMessage.get(), + error); + env->Throw(reinterpret_cast<jthrowable>(exception)); +} + static void android_net_utils_attachDhcpFilter(JNIEnv *env, jobject clazz, jobject javaFd) { struct sock_filter filter_code[] = { @@ -372,6 +400,63 @@ static void android_net_utils_addArpEntry(JNIEnv *env, jobject thiz, jbyteArray } } +static jobject android_net_utils_resNetworkQuery(JNIEnv *env, jobject thiz, jint netId, + jstring dname, jint ns_class, jint ns_type, jint flags) { + const jsize javaCharsCount = env->GetStringLength(dname); + const jsize byteCountUTF8 = env->GetStringUTFLength(dname); + + // Only allow dname which could be simply formatted to UTF8. + // In native layer, res_mkquery would re-format the input char array to packet. + std::vector<char> queryname(byteCountUTF8 + 1, 0); + + env->GetStringUTFRegion(dname, 0, javaCharsCount, queryname.data()); + int fd = resNetworkQuery(netId, queryname.data(), ns_class, ns_type, flags); + + if (fd < 0) { + throwErrnoException(env, "resNetworkQuery", -fd); + return nullptr; + } + + return jniCreateFileDescriptor(env, fd); +} + +static jobject android_net_utils_resNetworkSend(JNIEnv *env, jobject thiz, jint netId, + jbyteArray msg, jint msgLen, jint flags) { + uint8_t data[MAXCMDSIZE]; + + checkLenAndCopy(env, msg, msgLen, data); + int fd = resNetworkSend(netId, data, msgLen, flags); + + if (fd < 0) { + throwErrnoException(env, "resNetworkSend", -fd); + return nullptr; + } + + return jniCreateFileDescriptor(env, fd); +} + +static jbyteArray android_net_utils_resNetworkResult(JNIEnv *env, jobject thiz, jobject javaFd) { + int fd = jniGetFDFromFileDescriptor(env, javaFd); + int rcode; + std::vector<uint8_t> buf(MAXPACKETSIZE, 0); + + int res = resNetworkResult(fd, &rcode, buf.data(), MAXPACKETSIZE); + if (res < 0) { + throwErrnoException(env, "resNetworkResult", -res); + return nullptr; + } + + jbyteArray answer = env->NewByteArray(res); + if (answer == nullptr) { + throwErrnoException(env, "resNetworkResult", ENOMEM); + return nullptr; + } else { + env->SetByteArrayRegion(answer, 0, res, + reinterpret_cast<jbyte*>(buf.data())); + } + + return answer; +} // ---------------------------------------------------------------------------- @@ -391,6 +476,9 @@ static const JNINativeMethod gNetworkUtilMethods[] = { { "attachRaFilter", "(Ljava/io/FileDescriptor;I)V", (void*) android_net_utils_attachRaFilter }, { "attachControlPacketFilter", "(Ljava/io/FileDescriptor;I)V", (void*) android_net_utils_attachControlPacketFilter }, { "setupRaSocket", "(Ljava/io/FileDescriptor;I)V", (void*) android_net_utils_setupRaSocket }, + { "resNetworkSend", "(I[BII)Ljava/io/FileDescriptor;", (void*) android_net_utils_resNetworkSend }, + { "resNetworkQuery", "(ILjava/lang/String;III)Ljava/io/FileDescriptor;", (void*) android_net_utils_resNetworkQuery }, + { "resNetworkResult", "(Ljava/io/FileDescriptor;)[B", (void*) android_net_utils_resNetworkResult }, }; int register_android_net_NetworkUtils(JNIEnv* env) diff --git a/tests/net/java/android/net/DnsPacketTest.java b/tests/net/java/android/net/DnsPacketTest.java new file mode 100644 index 000000000000..032e52666970 --- /dev/null +++ b/tests/net/java/android/net/DnsPacketTest.java @@ -0,0 +1,159 @@ +/* + * 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 org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Arrays; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class DnsPacketTest { + private void assertHeaderParses(DnsPacket.DnsHeader header, int id, int flag, + int qCount, int aCount, int nsCount, int arCount) { + assertEquals(header.id, id); + assertEquals(header.flags, flag); + assertEquals(header.getSectionCount(DnsPacket.QDSECTION), qCount); + assertEquals(header.getSectionCount(DnsPacket.ANSECTION), aCount); + assertEquals(header.getSectionCount(DnsPacket.NSSECTION), nsCount); + assertEquals(header.getSectionCount(DnsPacket.ARSECTION), arCount); + } + + private void assertSectionParses(DnsPacket.DnsSection section, String dname, + int dtype, int dclass, int ttl, byte[] rr) { + assertEquals(section.dName, dname); + assertEquals(section.nsType, dtype); + assertEquals(section.nsClass, dclass); + assertEquals(section.ttl, ttl); + assertTrue(Arrays.equals(section.getRR(), rr)); + } + + class TestDnsPacket extends DnsPacket { + TestDnsPacket(byte[] data) throws ParseException { + super(data); + } + + public DnsHeader getHeader() { + return mHeader; + } + public List<DnsSection> getSectionList(int secType) { + return mSections[secType]; + } + } + + @Test + public void testNullDisallowed() { + try { + new TestDnsPacket(null); + fail("Exception not thrown for null byte array"); + } catch (DnsPacket.ParseException e) { + } + } + + @Test + public void testV4Answer() throws Exception { + final byte[] v4blob = new byte[] { + /* Header */ + 0x55, 0x66, /* Transaction ID */ + (byte) 0x81, (byte) 0x80, /* Flags */ + 0x00, 0x01, /* Questions */ + 0x00, 0x01, /* Answer RRs */ + 0x00, 0x00, /* Authority RRs */ + 0x00, 0x00, /* Additional RRs */ + /* Queries */ + 0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6F, 0x6F, 0x67, 0x6c, 0x65, + 0x03, 0x63, 0x6f, 0x6d, 0x00, /* Name */ + 0x00, 0x01, /* Type */ + 0x00, 0x01, /* Class */ + /* Answers */ + (byte) 0xc0, 0x0c, /* Name */ + 0x00, 0x01, /* Type */ + 0x00, 0x01, /* Class */ + 0x00, 0x00, 0x01, 0x2b, /* TTL */ + 0x00, 0x04, /* Data length */ + (byte) 0xac, (byte) 0xd9, (byte) 0xa1, (byte) 0x84 /* Address */ + }; + TestDnsPacket packet = new TestDnsPacket(v4blob); + + // Header part + assertHeaderParses(packet.getHeader(), 0x5566, 0x8180, 1, 1, 0, 0); + + // Section part + List<DnsPacket.DnsSection> qdSectionList = + packet.getSectionList(DnsPacket.QDSECTION); + assertEquals(qdSectionList.size(), 1); + assertSectionParses(qdSectionList.get(0), "www.google.com", 1, 1, 0, null); + + List<DnsPacket.DnsSection> anSectionList = + packet.getSectionList(DnsPacket.ANSECTION); + assertEquals(anSectionList.size(), 1); + assertSectionParses(anSectionList.get(0), "www.google.com", 1, 1, 0x12b, + new byte[]{ (byte) 0xac, (byte) 0xd9, (byte) 0xa1, (byte) 0x84 }); + } + + @Test + public void testV6Answer() throws Exception { + final byte[] v6blob = new byte[] { + /* Header */ + 0x77, 0x22, /* Transaction ID */ + (byte) 0x81, (byte) 0x80, /* Flags */ + 0x00, 0x01, /* Questions */ + 0x00, 0x01, /* Answer RRs */ + 0x00, 0x00, /* Authority RRs */ + 0x00, 0x00, /* Additional RRs */ + /* Queries */ + 0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6F, 0x6F, 0x67, 0x6c, 0x65, + 0x03, 0x63, 0x6f, 0x6d, 0x00, /* Name */ + 0x00, 0x1c, /* Type */ + 0x00, 0x01, /* Class */ + /* Answers */ + (byte) 0xc0, 0x0c, /* Name */ + 0x00, 0x1c, /* Type */ + 0x00, 0x01, /* Class */ + 0x00, 0x00, 0x00, 0x37, /* TTL */ + 0x00, 0x10, /* Data length */ + 0x24, 0x04, 0x68, 0x00, 0x40, 0x05, 0x08, 0x0d, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x04 /* Address */ + }; + TestDnsPacket packet = new TestDnsPacket(v6blob); + + // Header part + assertHeaderParses(packet.getHeader(), 0x7722, 0x8180, 1, 1, 0, 0); + + // Section part + List<DnsPacket.DnsSection> qdSectionList = + packet.getSectionList(DnsPacket.QDSECTION); + assertEquals(qdSectionList.size(), 1); + assertSectionParses(qdSectionList.get(0), "www.google.com", 28, 1, 0, null); + + List<DnsPacket.DnsSection> anSectionList = + packet.getSectionList(DnsPacket.ANSECTION); + assertEquals(anSectionList.size(), 1); + assertSectionParses(anSectionList.get(0), "www.google.com", 28, 1, 0x37, + new byte[]{ 0x24, 0x04, 0x68, 0x00, 0x40, 0x05, 0x08, 0x0d, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x04 }); + } +} |