NetworkDiagnostics.java revision 29f666688d73dcaf3f65b8124f34841927f70186
1/* 2 * Copyright (C) 2015 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 17package com.android.server.connectivity; 18 19import static android.system.OsConstants.*; 20 21import android.net.LinkAddress; 22import android.net.LinkProperties; 23import android.net.Network; 24import android.net.NetworkUtils; 25import android.net.RouteInfo; 26import android.os.SystemClock; 27import android.system.ErrnoException; 28import android.system.Os; 29import android.system.StructTimeval; 30import android.text.TextUtils; 31import android.util.Pair; 32 33import com.android.internal.util.IndentingPrintWriter; 34 35import java.io.Closeable; 36import java.io.FileDescriptor; 37import java.io.InterruptedIOException; 38import java.io.IOException; 39import java.net.Inet4Address; 40import java.net.Inet6Address; 41import java.net.InetAddress; 42import java.net.InetSocketAddress; 43import java.net.NetworkInterface; 44import java.net.SocketAddress; 45import java.net.SocketException; 46import java.net.UnknownHostException; 47import java.nio.ByteBuffer; 48import java.nio.charset.StandardCharsets; 49import java.util.concurrent.CountDownLatch; 50import java.util.concurrent.TimeUnit; 51import java.util.Arrays; 52import java.util.HashMap; 53import java.util.Map; 54import java.util.Random; 55 56import libcore.io.IoUtils; 57 58 59/** 60 * NetworkDiagnostics 61 * 62 * A simple class to diagnose network connectivity fundamentals. Current 63 * checks performed are: 64 * - ICMPv4/v6 echo requests for all routers 65 * - ICMPv4/v6 echo requests for all DNS servers 66 * - DNS UDP queries to all DNS servers 67 * 68 * Currently unimplemented checks include: 69 * - report ARP/ND data about on-link neighbors 70 * - DNS TCP queries to all DNS servers 71 * - HTTP DIRECT and PROXY checks 72 * - port 443 blocking/TLS intercept checks 73 * - QUIC reachability checks 74 * - MTU checks 75 * 76 * The supplied timeout bounds the entire diagnostic process. Each specific 77 * check class must implement this upper bound on measurements in whichever 78 * manner is most appropriate and effective. 79 * 80 * @hide 81 */ 82public class NetworkDiagnostics { 83 private static final String TAG = "NetworkDiagnostics"; 84 85 private static final InetAddress TEST_DNS4 = NetworkUtils.numericToInetAddress("8.8.8.8"); 86 private static final InetAddress TEST_DNS6 = NetworkUtils.numericToInetAddress( 87 "2001:4860:4860::8888"); 88 89 // For brevity elsewhere. 90 private static final long now() { 91 return SystemClock.elapsedRealtime(); 92 } 93 94 // Values from RFC 1035 section 4.1.1, names from <arpa/nameser.h>. 95 // Should be a member of DnsUdpCheck, but "compiler says no". 96 public static enum DnsResponseCode { NOERROR, FORMERR, SERVFAIL, NXDOMAIN, NOTIMP, REFUSED }; 97 98 private final Network mNetwork; 99 private final LinkProperties mLinkProperties; 100 private final Integer mInterfaceIndex; 101 102 private final long mTimeoutMs; 103 private final long mStartTime; 104 private final long mDeadlineTime; 105 106 // A counter, initialized to the total number of measurements, 107 // so callers can wait for completion. 108 private final CountDownLatch mCountDownLatch; 109 110 private class Measurement { 111 private static final String SUCCEEDED = "SUCCEEDED"; 112 private static final String FAILED = "FAILED"; 113 114 // TODO: Refactor to make these private for better encapsulation. 115 public String description = ""; 116 public long startTime; 117 public long finishTime; 118 public String result = ""; 119 public Thread thread; 120 121 public void recordSuccess(String msg) { 122 maybeFixupTimes(); 123 result = SUCCEEDED + ": " + msg; 124 if (mCountDownLatch != null) { 125 mCountDownLatch.countDown(); 126 } 127 } 128 129 public void recordFailure(String msg) { 130 maybeFixupTimes(); 131 result = FAILED + ": " + msg; 132 if (mCountDownLatch != null) { 133 mCountDownLatch.countDown(); 134 } 135 } 136 137 private void maybeFixupTimes() { 138 // Allows the caller to just set success/failure and not worry 139 // about also setting the correct finishing time. 140 if (finishTime == 0) { finishTime = now(); } 141 142 // In cases where, for example, a failure has occurred before the 143 // measurement even began, fixup the start time to reflect as much. 144 if (startTime == 0) { startTime = finishTime; } 145 } 146 147 @Override 148 public String toString() { 149 return description + ": " + result + " (" + (finishTime - startTime) + "ms)"; 150 } 151 } 152 153 private final Map<InetAddress, Measurement> mIcmpChecks = new HashMap<>(); 154 private final Map<Pair<InetAddress, InetAddress>, Measurement> mExplicitSourceIcmpChecks = 155 new HashMap<>(); 156 private final Map<InetAddress, Measurement> mDnsUdpChecks = new HashMap<>(); 157 private final String mDescription; 158 159 160 public NetworkDiagnostics(Network network, LinkProperties lp, long timeoutMs) { 161 mNetwork = network; 162 mLinkProperties = lp; 163 mInterfaceIndex = getInterfaceIndex(mLinkProperties.getInterfaceName()); 164 mTimeoutMs = timeoutMs; 165 mStartTime = now(); 166 mDeadlineTime = mStartTime + mTimeoutMs; 167 168 // Hardcode measurements to TEST_DNS4 and TEST_DNS6 in order to test off-link connectivity. 169 // We are free to modify mLinkProperties with impunity because ConnectivityService passes us 170 // a copy and not the original object. It's easier to do it this way because we don't need 171 // to check whether the LinkProperties already contains these DNS servers because 172 // LinkProperties#addDnsServer checks for duplicates. 173 if (mLinkProperties.isReachable(TEST_DNS4)) { 174 mLinkProperties.addDnsServer(TEST_DNS4); 175 } 176 // TODO: we could use mLinkProperties.isReachable(TEST_DNS6) here, because we won't set any 177 // DNS servers for which isReachable() is false, but since this is diagnostic code, be extra 178 // careful. 179 if (mLinkProperties.hasGlobalIPv6Address() || mLinkProperties.hasIPv6DefaultRoute()) { 180 mLinkProperties.addDnsServer(TEST_DNS6); 181 } 182 183 for (RouteInfo route : mLinkProperties.getRoutes()) { 184 if (route.hasGateway()) { 185 InetAddress gateway = route.getGateway(); 186 prepareIcmpMeasurement(gateway); 187 if (route.isIPv6Default()) { 188 prepareExplicitSourceIcmpMeasurements(gateway); 189 } 190 } 191 } 192 for (InetAddress nameserver : mLinkProperties.getDnsServers()) { 193 prepareIcmpMeasurement(nameserver); 194 prepareDnsMeasurement(nameserver); 195 } 196 197 mCountDownLatch = new CountDownLatch(totalMeasurementCount()); 198 199 startMeasurements(); 200 201 mDescription = "ifaces{" + TextUtils.join(",", mLinkProperties.getAllInterfaceNames()) + "}" 202 + " index{" + mInterfaceIndex + "}" 203 + " network{" + mNetwork + "}" 204 + " nethandle{" + mNetwork.getNetworkHandle() + "}"; 205 } 206 207 private static Integer getInterfaceIndex(String ifname) { 208 try { 209 NetworkInterface ni = NetworkInterface.getByName(ifname); 210 return ni.getIndex(); 211 } catch (NullPointerException | SocketException e) { 212 return null; 213 } 214 } 215 216 private void prepareIcmpMeasurement(InetAddress target) { 217 if (!mIcmpChecks.containsKey(target)) { 218 Measurement measurement = new Measurement(); 219 measurement.thread = new Thread(new IcmpCheck(target, measurement)); 220 mIcmpChecks.put(target, measurement); 221 } 222 } 223 224 private void prepareExplicitSourceIcmpMeasurements(InetAddress target) { 225 for (LinkAddress l : mLinkProperties.getLinkAddresses()) { 226 InetAddress source = l.getAddress(); 227 if (source instanceof Inet6Address && l.isGlobalPreferred()) { 228 Pair<InetAddress, InetAddress> srcTarget = new Pair<>(source, target); 229 if (!mExplicitSourceIcmpChecks.containsKey(srcTarget)) { 230 Measurement measurement = new Measurement(); 231 measurement.thread = new Thread(new IcmpCheck(source, target, measurement)); 232 mExplicitSourceIcmpChecks.put(srcTarget, measurement); 233 } 234 } 235 } 236 } 237 238 private void prepareDnsMeasurement(InetAddress target) { 239 if (!mDnsUdpChecks.containsKey(target)) { 240 Measurement measurement = new Measurement(); 241 measurement.thread = new Thread(new DnsUdpCheck(target, measurement)); 242 mDnsUdpChecks.put(target, measurement); 243 } 244 } 245 246 private int totalMeasurementCount() { 247 return mIcmpChecks.size() + mExplicitSourceIcmpChecks.size() + mDnsUdpChecks.size(); 248 } 249 250 private void startMeasurements() { 251 for (Measurement measurement : mIcmpChecks.values()) { 252 measurement.thread.start(); 253 } 254 for (Measurement measurement : mExplicitSourceIcmpChecks.values()) { 255 measurement.thread.start(); 256 } 257 for (Measurement measurement : mDnsUdpChecks.values()) { 258 measurement.thread.start(); 259 } 260 } 261 262 public void waitForMeasurements() { 263 try { 264 mCountDownLatch.await(mDeadlineTime - now(), TimeUnit.MILLISECONDS); 265 } catch (InterruptedException ignored) {} 266 } 267 268 public void dump(IndentingPrintWriter pw) { 269 pw.println(TAG + ":" + mDescription); 270 final long unfinished = mCountDownLatch.getCount(); 271 if (unfinished > 0) { 272 // This can't happen unless a caller forgets to call waitForMeasurements() 273 // or a measurement isn't implemented to correctly honor the timeout. 274 pw.println("WARNING: countdown wait incomplete: " 275 + unfinished + " unfinished measurements"); 276 } 277 278 pw.increaseIndent(); 279 for (Map.Entry<InetAddress, Measurement> entry : mIcmpChecks.entrySet()) { 280 if (entry.getKey() instanceof Inet4Address) { 281 pw.println(entry.getValue().toString()); 282 } 283 } 284 for (Map.Entry<InetAddress, Measurement> entry : mIcmpChecks.entrySet()) { 285 if (entry.getKey() instanceof Inet6Address) { 286 pw.println(entry.getValue().toString()); 287 } 288 } 289 for (Map.Entry<Pair<InetAddress, InetAddress>, Measurement> entry : 290 mExplicitSourceIcmpChecks.entrySet()) { 291 pw.println(entry.getValue().toString()); 292 } 293 for (Map.Entry<InetAddress, Measurement> entry : mDnsUdpChecks.entrySet()) { 294 if (entry.getKey() instanceof Inet4Address) { 295 pw.println(entry.getValue().toString()); 296 } 297 } 298 for (Map.Entry<InetAddress, Measurement> entry : mDnsUdpChecks.entrySet()) { 299 if (entry.getKey() instanceof Inet6Address) { 300 pw.println(entry.getValue().toString()); 301 } 302 } 303 pw.decreaseIndent(); 304 } 305 306 307 private class SimpleSocketCheck implements Closeable { 308 protected final InetAddress mSource; // Usually null. 309 protected final InetAddress mTarget; 310 protected final int mAddressFamily; 311 protected final Measurement mMeasurement; 312 protected FileDescriptor mFileDescriptor; 313 protected SocketAddress mSocketAddress; 314 315 protected SimpleSocketCheck( 316 InetAddress source, InetAddress target, Measurement measurement) { 317 mMeasurement = measurement; 318 319 if (target instanceof Inet6Address) { 320 Inet6Address targetWithScopeId = null; 321 if (target.isLinkLocalAddress() && mInterfaceIndex != null) { 322 try { 323 targetWithScopeId = Inet6Address.getByAddress( 324 null, target.getAddress(), mInterfaceIndex); 325 } catch (UnknownHostException e) { 326 mMeasurement.recordFailure(e.toString()); 327 } 328 } 329 mTarget = (targetWithScopeId != null) ? targetWithScopeId : target; 330 mAddressFamily = AF_INET6; 331 } else { 332 mTarget = target; 333 mAddressFamily = AF_INET; 334 } 335 336 // We don't need to check the scope ID here because we currently only do explicit-source 337 // measurements from global IPv6 addresses. 338 mSource = source; 339 } 340 341 protected SimpleSocketCheck(InetAddress target, Measurement measurement) { 342 this(null, target, measurement); 343 } 344 345 protected void setupSocket( 346 int sockType, int protocol, long writeTimeout, long readTimeout, int dstPort) 347 throws ErrnoException, IOException { 348 mFileDescriptor = Os.socket(mAddressFamily, sockType, protocol); 349 // Setting SNDTIMEO is purely for defensive purposes. 350 Os.setsockoptTimeval(mFileDescriptor, 351 SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(writeTimeout)); 352 Os.setsockoptTimeval(mFileDescriptor, 353 SOL_SOCKET, SO_RCVTIMEO, StructTimeval.fromMillis(readTimeout)); 354 // TODO: Use IP_RECVERR/IPV6_RECVERR, pending OsContants availability. 355 mNetwork.bindSocket(mFileDescriptor); 356 if (mSource != null) { 357 Os.bind(mFileDescriptor, mSource, 0); 358 } 359 Os.connect(mFileDescriptor, mTarget, dstPort); 360 mSocketAddress = Os.getsockname(mFileDescriptor); 361 } 362 363 protected String getSocketAddressString() { 364 // The default toString() implementation is not the prettiest. 365 InetSocketAddress inetSockAddr = (InetSocketAddress) mSocketAddress; 366 InetAddress localAddr = inetSockAddr.getAddress(); 367 return String.format( 368 (localAddr instanceof Inet6Address ? "[%s]:%d" : "%s:%d"), 369 localAddr.getHostAddress(), inetSockAddr.getPort()); 370 } 371 372 @Override 373 public void close() { 374 IoUtils.closeQuietly(mFileDescriptor); 375 } 376 } 377 378 379 private class IcmpCheck extends SimpleSocketCheck implements Runnable { 380 private static final int TIMEOUT_SEND = 100; 381 private static final int TIMEOUT_RECV = 300; 382 private static final int ICMPV4_ECHO_REQUEST = 8; 383 private static final int ICMPV6_ECHO_REQUEST = 128; 384 private static final int PACKET_BUFSIZE = 512; 385 private final int mProtocol; 386 private final int mIcmpType; 387 388 public IcmpCheck(InetAddress source, InetAddress target, Measurement measurement) { 389 super(source, target, measurement); 390 391 if (mAddressFamily == AF_INET6) { 392 mProtocol = IPPROTO_ICMPV6; 393 mIcmpType = ICMPV6_ECHO_REQUEST; 394 mMeasurement.description = "ICMPv6"; 395 } else { 396 mProtocol = IPPROTO_ICMP; 397 mIcmpType = ICMPV4_ECHO_REQUEST; 398 mMeasurement.description = "ICMPv4"; 399 } 400 401 mMeasurement.description += " dst{" + mTarget.getHostAddress() + "}"; 402 } 403 404 public IcmpCheck(InetAddress target, Measurement measurement) { 405 this(null, target, measurement); 406 } 407 408 @Override 409 public void run() { 410 // Check if this measurement has already failed during setup. 411 if (mMeasurement.finishTime > 0) { 412 // If the measurement failed during construction it didn't 413 // decrement the countdown latch; do so here. 414 mCountDownLatch.countDown(); 415 return; 416 } 417 418 try { 419 setupSocket(SOCK_DGRAM, mProtocol, TIMEOUT_SEND, TIMEOUT_RECV, 0); 420 } catch (ErrnoException | IOException e) { 421 mMeasurement.recordFailure(e.toString()); 422 return; 423 } 424 mMeasurement.description += " src{" + getSocketAddressString() + "}"; 425 426 // Build a trivial ICMP packet. 427 final byte[] icmpPacket = { 428 (byte) mIcmpType, 0, 0, 0, 0, 0, 0, 0 // ICMP header 429 }; 430 431 int count = 0; 432 mMeasurement.startTime = now(); 433 while (now() < mDeadlineTime - (TIMEOUT_SEND + TIMEOUT_RECV)) { 434 count++; 435 icmpPacket[icmpPacket.length - 1] = (byte) count; 436 try { 437 Os.write(mFileDescriptor, icmpPacket, 0, icmpPacket.length); 438 } catch (ErrnoException | InterruptedIOException e) { 439 mMeasurement.recordFailure(e.toString()); 440 break; 441 } 442 443 try { 444 ByteBuffer reply = ByteBuffer.allocate(PACKET_BUFSIZE); 445 Os.read(mFileDescriptor, reply); 446 // TODO: send a few pings back to back to guesstimate packet loss. 447 mMeasurement.recordSuccess("1/" + count); 448 break; 449 } catch (ErrnoException | InterruptedIOException e) { 450 continue; 451 } 452 } 453 if (mMeasurement.finishTime == 0) { 454 mMeasurement.recordFailure("0/" + count); 455 } 456 457 close(); 458 } 459 } 460 461 462 private class DnsUdpCheck extends SimpleSocketCheck implements Runnable { 463 private static final int TIMEOUT_SEND = 100; 464 private static final int TIMEOUT_RECV = 500; 465 private static final int DNS_SERVER_PORT = 53; 466 private static final int RR_TYPE_A = 1; 467 private static final int RR_TYPE_AAAA = 28; 468 private static final int PACKET_BUFSIZE = 512; 469 470 private final Random mRandom = new Random(); 471 472 // Should be static, but the compiler mocks our puny, human attempts at reason. 473 private String responseCodeStr(int rcode) { 474 try { 475 return DnsResponseCode.values()[rcode].toString(); 476 } catch (IndexOutOfBoundsException e) { 477 return String.valueOf(rcode); 478 } 479 } 480 481 private final int mQueryType; 482 483 public DnsUdpCheck(InetAddress target, Measurement measurement) { 484 super(target, measurement); 485 486 // TODO: Ideally, query the target for both types regardless of address family. 487 if (mAddressFamily == AF_INET6) { 488 mQueryType = RR_TYPE_AAAA; 489 } else { 490 mQueryType = RR_TYPE_A; 491 } 492 493 mMeasurement.description = "DNS UDP dst{" + mTarget.getHostAddress() + "}"; 494 } 495 496 @Override 497 public void run() { 498 // Check if this measurement has already failed during setup. 499 if (mMeasurement.finishTime > 0) { 500 // If the measurement failed during construction it didn't 501 // decrement the countdown latch; do so here. 502 mCountDownLatch.countDown(); 503 return; 504 } 505 506 try { 507 setupSocket(SOCK_DGRAM, IPPROTO_UDP, TIMEOUT_SEND, TIMEOUT_RECV, DNS_SERVER_PORT); 508 } catch (ErrnoException | IOException e) { 509 mMeasurement.recordFailure(e.toString()); 510 return; 511 } 512 mMeasurement.description += " src{" + getSocketAddressString() + "}"; 513 514 // This needs to be fixed length so it can be dropped into the pre-canned packet. 515 final String sixRandomDigits = 516 Integer.valueOf(mRandom.nextInt(900000) + 100000).toString(); 517 mMeasurement.description += " qtype{" + mQueryType + "}" 518 + " qname{" + sixRandomDigits + "-android-ds.metric.gstatic.com}"; 519 520 // Build a trivial DNS packet. 521 final byte[] dnsPacket = getDnsQueryPacket(sixRandomDigits); 522 523 int count = 0; 524 mMeasurement.startTime = now(); 525 while (now() < mDeadlineTime - (TIMEOUT_RECV + TIMEOUT_RECV)) { 526 count++; 527 try { 528 Os.write(mFileDescriptor, dnsPacket, 0, dnsPacket.length); 529 } catch (ErrnoException | InterruptedIOException e) { 530 mMeasurement.recordFailure(e.toString()); 531 break; 532 } 533 534 try { 535 ByteBuffer reply = ByteBuffer.allocate(PACKET_BUFSIZE); 536 Os.read(mFileDescriptor, reply); 537 // TODO: more correct and detailed evaluation of the response, 538 // possibly adding the returned IP address(es) to the output. 539 final String rcodeStr = (reply.limit() > 3) 540 ? " " + responseCodeStr((int) (reply.get(3)) & 0x0f) 541 : ""; 542 mMeasurement.recordSuccess("1/" + count + rcodeStr); 543 break; 544 } catch (ErrnoException | InterruptedIOException e) { 545 continue; 546 } 547 } 548 if (mMeasurement.finishTime == 0) { 549 mMeasurement.recordFailure("0/" + count); 550 } 551 552 close(); 553 } 554 555 private byte[] getDnsQueryPacket(String sixRandomDigits) { 556 byte[] rnd = sixRandomDigits.getBytes(StandardCharsets.US_ASCII); 557 return new byte[] { 558 (byte) mRandom.nextInt(), (byte) mRandom.nextInt(), // [0-1] query ID 559 1, 0, // [2-3] flags; byte[2] = 1 for recursion desired (RD). 560 0, 1, // [4-5] QDCOUNT (number of queries) 561 0, 0, // [6-7] ANCOUNT (number of answers) 562 0, 0, // [8-9] NSCOUNT (number of name server records) 563 0, 0, // [10-11] ARCOUNT (number of additional records) 564 17, rnd[0], rnd[1], rnd[2], rnd[3], rnd[4], rnd[5], 565 '-', 'a', 'n', 'd', 'r', 'o', 'i', 'd', '-', 'd', 's', 566 6, 'm', 'e', 't', 'r', 'i', 'c', 567 7, 'g', 's', 't', 'a', 't', 'i', 'c', 568 3, 'c', 'o', 'm', 569 0, // null terminator of FQDN (root TLD) 570 0, (byte) mQueryType, // QTYPE 571 0, 1 // QCLASS, set to 1 = IN (Internet) 572 }; 573 } 574 } 575} 576