• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.android.iwlan.epdg;
18 
19 import android.annotation.CallbackExecutor;
20 import android.annotation.IntDef;
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.net.DnsResolver;
24 import android.net.DnsResolver.DnsException;
25 import android.net.Network;
26 import android.net.ParseException;
27 import android.os.CancellationSignal;
28 import android.util.Log;
29 
30 import com.android.net.module.util.DnsPacket;
31 import com.android.net.module.util.DnsPacketUtils.DnsRecordParser;
32 
33 import java.lang.annotation.Retention;
34 import java.lang.annotation.RetentionPolicy;
35 import java.nio.BufferUnderflowException;
36 import java.nio.ByteBuffer;
37 import java.nio.charset.StandardCharsets;
38 import java.util.ArrayList;
39 import java.util.List;
40 import java.util.concurrent.Executor;
41 import java.util.concurrent.Executors;
42 
43 /**
44  * A utility wrapper around android.net.DnsResolver that queries for NAPTR DNS Resource Records, and
45  * returns in the user callback a list of server (IP addresses, port number) combinations pertaining
46  * to the service requested.
47  */
48 final class NaptrDnsResolver {
49     private static final String TAG = "NaptrDnsResolver";
50 
51     @IntDef(
52             prefix = {"TYPE_"},
53             value = {TYPE_A, TYPE_SRV, TYPE_U, TYPE_P})
54     @Retention(RetentionPolicy.SOURCE)
55     @interface NaptrRecordType {}
56 
57     public static final int TYPE_A = 0;
58     public static final int TYPE_SRV = 1;
59 
60     // These below record types are not currently supported.
61     public static final int TYPE_U = 2;
62     public static final int TYPE_P = 3;
63 
64     /**
65      * A NAPTR record comprises of a target domain name, along with the type of the record, as
66      * defined in RFC 2915.
67      */
68     static class NaptrTarget {
69         public final String mName;
70         public final int mType;
71 
NaptrTarget(String name, @NaptrRecordType int type)72         public NaptrTarget(String name, @NaptrRecordType int type) {
73             mName = name;
74             mType = type;
75         }
76     }
77 
78     static final int QUERY_TYPE_NAPTR = 35;
79 
80     static class NaptrResponse extends DnsPacket {
81         /*
82          * Parses and stores a NAPTR record as described in RFC 2915.
83          */
84         static class NaptrRecord {
85 
86             // A 16-bit unsigned value- the client should prioritize records with lower
87             // 'preference'.
88             public final int preference;
89 
90             // A 16-bit unsigned value- for records with the same 'preference' field, the client
91             // should prioritize records with lower 'order'.
92             public final int order;
93 
94             // A string that denotes the @NaptrRecordType.
95             @NonNull public final String flag;
96 
97             // A free form string that denotes the service provided by the server described by the
98             // record- SIP, email, etc.
99             public final String service;
100 
101             // A string that describes the regex transformation (described in RFC 2915) that needs
102             // to be applied to the DNS query domain name for further processes. RFC 2915 describes
103             // that in a NaptrRecord, exactly one of |regex| and |replacement| must be non-null.
104             @Nullable public final String regex;
105 
106             // This string describes how the input DNS query domain name should be replaced. With
107             // the 'flag' and 'service' field, this instructs the DNS client on what to do next.
108             // For current use cases, only the |replacement| field is expected to be non-null in a
109             // NaptrRecord.
110             @Nullable public final String replacement;
111 
112             private static final int MAXNAMESIZE = 255;
113 
parseNextField(ByteBuffer buf)114             private String parseNextField(ByteBuffer buf) throws BufferUnderflowException {
115                 final short size = buf.get();
116                 // size can also be 0, for instance for the 'regex' field.
117                 final byte[] field = new byte[size];
118                 buf.get(field, 0, size);
119                 return new String(field, StandardCharsets.UTF_8);
120             }
121 
122             @NaptrRecordType
getTypeFromFlagString()123             public int getTypeFromFlagString() {
124                 switch (flag) {
125                     case "S":
126                     case "s":
127                         return TYPE_SRV;
128                     case "A":
129                     case "a":
130                         return TYPE_A;
131                     default:
132                         throw new ParseException("Unsupported flag type: " + flag);
133                 }
134             }
135 
NaptrRecord(byte[] naptrRecordData)136             NaptrRecord(byte[] naptrRecordData) throws ParseException {
137                 final ByteBuffer buf = ByteBuffer.wrap(naptrRecordData);
138                 try {
139                     order = Short.toUnsignedInt(buf.getShort());
140                     preference = Short.toUnsignedInt(buf.getShort());
141                     flag = parseNextField(buf);
142                     service = parseNextField(buf);
143                     regex = parseNextField(buf);
144                     if (regex.length() != 0) {
145                         throw new ParseException("NAPTR: regex field expected to be empty!");
146                     }
147                     replacement =
148                             DnsRecordParser.parseName(
149                                     buf, 0, /* isNameCompressionSupported */ true);
150                     if (replacement == null) {
151                         throw new ParseException(
152                                 "NAPTR: replacement field not expected to be empty!");
153                     }
154                     if (replacement.length() > MAXNAMESIZE) {
155                         throw new ParseException(
156                                 "Parse name fail, replacement name size is too long: "
157                                         + replacement.length());
158                     }
159                     if (buf.hasRemaining()) {
160                         throw new ParseException(
161                                 "Parsing NAPTR record data failed: more bytes than expected!");
162                     }
163                 } catch (BufferUnderflowException e) {
164                     throw new ParseException("Parsing NAPTR Record data failed with cause", e);
165                 }
166             }
167         }
168 
169         private final int mQueryType;
170 
NaptrResponse(@onNull byte[] data)171         NaptrResponse(@NonNull byte[] data) throws ParseException {
172             super(data);
173             if (!mHeader.isResponse()) {
174                 throw new ParseException("Not an answer packet");
175             }
176             int numQueries = mHeader.getRecordCount(QDSECTION);
177             // Expects exactly one query in query section.
178             if (numQueries != 1) {
179                 throw new ParseException("Unexpected query count: " + numQueries);
180             }
181             // Expect only one question in question section.
182             mQueryType = mRecords[QDSECTION].get(0).nsType;
183             if (mQueryType != QUERY_TYPE_NAPTR) {
184                 throw new ParseException("Unexpected query type: " + mQueryType);
185             }
186         }
187 
parseNaptrRecords()188         public @NonNull List<NaptrRecord> parseNaptrRecords() throws ParseException {
189             final List<NaptrRecord> naptrRecords = new ArrayList<>();
190             if (mHeader.getRecordCount(ANSECTION) == 0) return naptrRecords;
191 
192             for (final DnsRecord ansSec : mRecords[ANSECTION]) {
193                 final int nsType = ansSec.nsType;
194                 if (nsType != QUERY_TYPE_NAPTR) {
195                     throw new ParseException("Unexpected DNS record type in ANSECTION: " + nsType);
196                 }
197                 final NaptrRecord record = new NaptrRecord(ansSec.getRR());
198                 naptrRecords.add(record);
199                 Log.d(
200                         TAG,
201                         "NaptrRecord name: "
202                                 + ansSec.dName
203                                 + " replacement field: "
204                                 + record.replacement);
205             }
206             return naptrRecords;
207         }
208     }
209 
210     /**
211      * A decorator for DnsResolver.Callback that accumulates IPv4/v6 responses for NAPTR DNS queries
212      * and passes it up to the user callback.
213      */
214     static class NaptrRecordAnswerAccumulator implements DnsResolver.Callback<byte[]> {
215         private static final String TAG = "NaptrRecordAnswerAccum";
216 
217         private final DnsResolver.Callback<List<NaptrTarget>> mUserCallback;
218         private final Executor mUserExecutor;
219 
220         private static class LazyExecutor {
221             public static final Executor INSTANCE = Executors.newSingleThreadExecutor();
222         }
223 
getInternalExecutor()224         static Executor getInternalExecutor() {
225             return LazyExecutor.INSTANCE;
226         }
227 
NaptrRecordAnswerAccumulator( @onNull DnsResolver.Callback<List<NaptrTarget>> callback, @NonNull @CallbackExecutor Executor executor)228         NaptrRecordAnswerAccumulator(
229                 @NonNull DnsResolver.Callback<List<NaptrTarget>> callback,
230                 @NonNull @CallbackExecutor Executor executor) {
231             mUserCallback = callback;
232             mUserExecutor = executor;
233         }
234 
composeNaptrRecordResult( List<NaptrResponse.NaptrRecord> responses)235         private List<NaptrTarget> composeNaptrRecordResult(
236                 List<NaptrResponse.NaptrRecord> responses) throws ParseException {
237             final List<NaptrTarget> records = new ArrayList<>();
238             if (responses.isEmpty()) return records;
239             for (NaptrResponse.NaptrRecord response : responses) {
240                 records.add(
241                         new NaptrTarget(response.replacement, response.getTypeFromFlagString()));
242             }
243             return records;
244         }
245 
246         @Override
onAnswer(@onNull byte[] answer, int rcode)247         public void onAnswer(@NonNull byte[] answer, int rcode) {
248             try {
249                 final NaptrResponse response = new NaptrResponse(answer);
250                 final List<NaptrTarget> result =
251                         composeNaptrRecordResult(response.parseNaptrRecords());
252                 mUserExecutor.execute(() -> mUserCallback.onAnswer(result, rcode));
253             } catch (DnsPacket.ParseException e) {
254                 // Convert the com.android.net.module.util.DnsPacket.ParseException to an
255                 // android.net.ParseException. This is the type that was used in Q and is implied
256                 // by the public documentation of ERROR_PARSE.
257                 //
258                 // DnsPacket cannot throw android.net.ParseException directly because it's @hide.
259                 final ParseException pe = new ParseException(e.reason, e.getCause());
260                 pe.setStackTrace(e.getStackTrace());
261                 Log.e(TAG, "ParseException", pe);
262                 mUserExecutor.execute(
263                         () -> mUserCallback.onError(new DnsException(DnsResolver.ERROR_PARSE, pe)));
264             }
265         }
266 
267         @Override
onError(@onNull DnsException error)268         public void onError(@NonNull DnsException error) {
269             Log.e(TAG, "onError: " + error);
270             mUserExecutor.execute(() -> mUserCallback.onError(error));
271         }
272     }
273 
274     /**
275      * Send an NAPTR DNS query on the specified network. The answer will be provided asynchronously
276      * on the passed executor, through the provided {@link DnsResolver.Callback}.
277      *
278      * @param network {@link Network} specifying which network to query on. {@code null} for query
279      *     on default network.
280      * @param domain NAPTR domain name to query.
281      * @param cancellationSignal used by the caller to signal if the query should be cancelled. May
282      *     be {@code null}.
283      * @param callback a {@link DnsResolver.Callback} which will be called on a separate thread to
284      *     notify the caller of the result of dns query.
285      */
query( @ullable Network network, @NonNull String domain, @NonNull @CallbackExecutor Executor executor, @Nullable CancellationSignal cancellationSignal, @NonNull DnsResolver.Callback<List<NaptrTarget>> callback)286     public static void query(
287             @Nullable Network network,
288             @NonNull String domain,
289             @NonNull @CallbackExecutor Executor executor,
290             @Nullable CancellationSignal cancellationSignal,
291             @NonNull DnsResolver.Callback<List<NaptrTarget>> callback) {
292         final NaptrRecordAnswerAccumulator naptrDnsCb =
293                 new NaptrRecordAnswerAccumulator(callback, executor);
294         DnsResolver.getInstance()
295                 .rawQuery(
296                         network,
297                         domain,
298                         DnsResolver.CLASS_IN,
299                         QUERY_TYPE_NAPTR,
300                         DnsResolver.FLAG_EMPTY,
301                         NaptrRecordAnswerAccumulator.getInternalExecutor(),
302                         cancellationSignal,
303                         naptrDnsCb);
304     }
305 
NaptrDnsResolver()306     private NaptrDnsResolver() {}
307 }
308