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