Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 31ff0616 authored by Luke Huang's avatar Luke Huang Committed by Gerrit Code Review
Browse files

Merge changes from topic "Java async dns"

* changes:
  Add asynchronous DNS query API in Java
  Add jni method for DnsResolver
parents 625a1f44 00b15f33
Loading
Loading
Loading
Loading
+22 −0
Original line number Diff line number Diff line
@@ -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);
+235 −0
Original line number Diff line number Diff line
/*
 * 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);
                }
            }
        }
    }
}
+289 −0
Original line number Diff line number Diff line
/*
 * 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);
            }
        }
    }
}
+28 −0
Original line number Diff line number Diff line
@@ -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.
 *
@@ -132,6 +134,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.
     */
+88 −0

File changed.

Preview size limit exceeded, changes collapsed.

Loading