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

Commit 00b15f33 authored by Luke Huang's avatar Luke Huang
Browse files

Add asynchronous DNS query API in Java

DnsResolver for asynchronous DNS querying
DnsPacket for parsing answer

Test: built, flashed, booted
      atest DnsResolverTest
      atest DnsPacketTest

Change-Id: Id014bc7387dd940cfaa270f68e7d4d85fab320a0
parent 92ae35ec
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);
            }
        }
    }
}
+159 −0

File added.

Preview size limit exceeded, changes collapsed.