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