From 9647f3807b2b1b3b873338263c40c748fbd0c7cb Mon Sep 17 00:00:00 2001 From: Erik Kline Date: Fri, 5 Jun 2015 17:47:34 +0900 Subject: [PATCH] initial revision of NetworkDiagnostics Bug: 21449922 Change-Id: I6a66cfa98601d6ccfe38ff840b0dd49ed1538a41 --- .../android/server/ConnectivityService.java | 30 ++ .../connectivity/NetworkDiagnostics.java | 509 ++++++++++++++++++ 2 files changed, 539 insertions(+) create mode 100644 services/core/java/com/android/server/connectivity/NetworkDiagnostics.java diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java index 99c4eda62c..8716101f40 100644 --- a/services/core/java/com/android/server/ConnectivityService.java +++ b/services/core/java/com/android/server/ConnectivityService.java @@ -112,6 +112,7 @@ import com.android.internal.util.IndentingPrintWriter; import com.android.internal.util.XmlUtils; import com.android.server.am.BatteryStatsService; import com.android.server.connectivity.DataConnectionStats; +import com.android.server.connectivity.NetworkDiagnostics; import com.android.server.connectivity.Nat464Xlat; import com.android.server.connectivity.NetworkAgentInfo; import com.android.server.connectivity.NetworkMonitor; @@ -1749,6 +1750,15 @@ public class ConnectivityService extends IConnectivityManager.Stub return ret; } + private boolean shouldPerformDiagnostics(String[] args) { + for (String arg : args) { + if (arg.equals("--diag")) { + return true; + } + } + return false; + } + @Override protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " "); @@ -1761,6 +1771,26 @@ public class ConnectivityService extends IConnectivityManager.Stub return; } + final List netDiags = new ArrayList(); + if (shouldPerformDiagnostics(args)) { + final long DIAG_TIME_MS = 5000; + for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) { + // Start gathering diagnostic information. + netDiags.add(new NetworkDiagnostics( + nai.network, + new LinkProperties(nai.linkProperties), + DIAG_TIME_MS)); + } + + for (NetworkDiagnostics netDiag : netDiags) { + pw.println(); + netDiag.waitForMeasurements(); + netDiag.dump(pw); + } + + return; + } + pw.print("NetworkFactories for:"); for (NetworkFactoryInfo nfi : mNetworkFactoryInfos.values()) { pw.print(" " + nfi.name); diff --git a/services/core/java/com/android/server/connectivity/NetworkDiagnostics.java b/services/core/java/com/android/server/connectivity/NetworkDiagnostics.java new file mode 100644 index 0000000000..5d56d4aeb8 --- /dev/null +++ b/services/core/java/com/android/server/connectivity/NetworkDiagnostics.java @@ -0,0 +1,509 @@ +/* + * Copyright (C) 2015 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 com.android.server.connectivity; + +import static android.system.OsConstants.*; + +import android.net.LinkProperties; +import android.net.Network; +import android.net.RouteInfo; +import android.os.SystemClock; +import android.system.ErrnoException; +import android.system.Os; +import android.system.StructTimeval; +import android.text.TextUtils; + +import com.android.internal.util.IndentingPrintWriter; + +import java.io.Closeable; +import java.io.FileDescriptor; +import java.io.InterruptedIOException; +import java.io.IOException; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.NetworkInterface; +import java.net.SocketAddress; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +import libcore.io.IoUtils; + + +/** + * NetworkDiagnostics + * + * A simple class to diagnose network connectivity fundamentals. Current + * checks performed are: + * - ICMPv4/v6 echo requests for all routers + * - ICMPv4/v6 echo requests for all DNS servers + * - DNS UDP queries to all DNS servers + * + * Currently unimplemented checks include: + * - report ARP/ND data about on-link neighbors + * - DNS TCP queries to all DNS servers + * - HTTP DIRECT and PROXY checks + * - port 443 blocking/TLS intercept checks + * - QUIC reachability checks + * - MTU checks + * + * The supplied timeout bounds the entire diagnostic process. Each specific + * check class must implement this upper bound on measurements in whichever + * manner is most appropriate and effective. + * + * @hide + */ +public class NetworkDiagnostics { + private static final String TAG = "NetworkDiagnostics"; + + // For brevity elsewhere. + private static final long now() { + return SystemClock.elapsedRealtime(); + } + + // Values from RFC 1035 section 4.1.1, names from . + // Should be a member of DnsUdpCheck, but "compiler says no". + public static enum DnsResponseCode { NOERROR, FORMERR, SERVFAIL, NXDOMAIN, NOTIMP, REFUSED }; + + private final Network mNetwork; + private final LinkProperties mLinkProperties; + private final Integer mInterfaceIndex; + + private final long mTimeoutMs; + private final long mStartTime; + private final long mDeadlineTime; + + // A counter, initialized to the total number of measurements, + // so callers can wait for completion. + private final CountDownLatch mCountDownLatch; + + private class Measurement { + private static final String SUCCEEDED = "SUCCEEDED"; + private static final String FAILED = "FAILED"; + + // TODO: Refactor to make these private for better encapsulation. + public String description = ""; + public long startTime; + public long finishTime; + public String result = ""; + public Thread thread; + + public void recordSuccess(String msg) { + maybeFixupTimes(); + if (mCountDownLatch != null) { + mCountDownLatch.countDown(); + } + result = SUCCEEDED + ": " + msg; + } + + public void recordFailure(String msg) { + maybeFixupTimes(); + if (mCountDownLatch != null) { + mCountDownLatch.countDown(); + } + result = FAILED + ": " + msg; + } + + private void maybeFixupTimes() { + // Allows the caller to just set success/failure and not worry + // about also setting the correct finishing time. + if (finishTime == 0) { finishTime = now(); } + + // In cases where, for example, a failure has occurred before the + // measurement even began, fixup the start time to reflect as much. + if (startTime == 0) { startTime = finishTime; } + } + + @Override + public String toString() { + return description + ": " + result + " (" + (finishTime - startTime) + "ms)"; + } + } + + private final Map mIcmpChecks = new HashMap<>(); + private final Map mDnsUdpChecks = new HashMap<>(); + private final String mDescription; + + + public NetworkDiagnostics(Network network, LinkProperties lp, long timeoutMs) { + mNetwork = network; + mLinkProperties = lp; + mInterfaceIndex = getInterfaceIndex(mLinkProperties.getInterfaceName()); + mTimeoutMs = timeoutMs; + mStartTime = now(); + mDeadlineTime = mStartTime + mTimeoutMs; + + for (RouteInfo route : mLinkProperties.getRoutes()) { + if (route.hasGateway()) { + prepareIcmpMeasurement(route.getGateway()); + } + } + for (InetAddress nameserver : mLinkProperties.getDnsServers()) { + prepareIcmpMeasurement(nameserver); + prepareDnsMeasurement(nameserver); + } + + mCountDownLatch = new CountDownLatch(totalMeasurementCount()); + + startMeasurements(); + + mDescription = "ifaces{" + TextUtils.join(",", mLinkProperties.getAllInterfaceNames()) + "}" + + " index{" + mInterfaceIndex + "}" + + " network{" + mNetwork + "}" + + " nethandle{" + mNetwork.getNetworkHandle() + "}"; + } + + private static Integer getInterfaceIndex(String ifname) { + try { + NetworkInterface ni = NetworkInterface.getByName(ifname); + return ni.getIndex(); + } catch (NullPointerException | SocketException e) { + return null; + } + } + + private void prepareIcmpMeasurement(InetAddress target) { + if (!mIcmpChecks.containsKey(target)) { + Measurement measurement = new Measurement(); + measurement.thread = new Thread(new IcmpCheck(target, measurement)); + mIcmpChecks.put(target, measurement); + } + } + + private void prepareDnsMeasurement(InetAddress target) { + if (!mDnsUdpChecks.containsKey(target)) { + Measurement measurement = new Measurement(); + measurement.thread = new Thread(new DnsUdpCheck(target, measurement)); + mDnsUdpChecks.put(target, measurement); + } + } + + private int totalMeasurementCount() { + return mIcmpChecks.size() + mDnsUdpChecks.size(); + } + + private void startMeasurements() { + for (Measurement measurement : mIcmpChecks.values()) { + measurement.thread.start(); + } + for (Measurement measurement : mDnsUdpChecks.values()) { + measurement.thread.start(); + } + } + + public void waitForMeasurements() { + try { + mCountDownLatch.await(mDeadlineTime - now(), TimeUnit.MILLISECONDS); + } catch (InterruptedException ignored) {} + } + + public void dump(IndentingPrintWriter pw) { + pw.println(TAG + ":" + mDescription); + final long unfinished = mCountDownLatch.getCount(); + if (unfinished > 0) { + // This can't happen unless a caller forgets to call waitForMeasurements() + // or a measurement isn't implemented to correctly honor the timeout. + pw.println("WARNING: countdown wait incomplete: " + + unfinished + " unfinished measurements"); + } + + pw.increaseIndent(); + for (Map.Entry entry : mIcmpChecks.entrySet()) { + if (entry.getKey() instanceof Inet4Address) { + pw.println(entry.getValue().toString()); + } + } + for (Map.Entry entry : mIcmpChecks.entrySet()) { + if (entry.getKey() instanceof Inet6Address) { + pw.println(entry.getValue().toString()); + } + } + for (Map.Entry entry : mDnsUdpChecks.entrySet()) { + if (entry.getKey() instanceof Inet4Address) { + pw.println(entry.getValue().toString()); + } + } + for (Map.Entry entry : mDnsUdpChecks.entrySet()) { + if (entry.getKey() instanceof Inet6Address) { + pw.println(entry.getValue().toString()); + } + } + pw.decreaseIndent(); + } + + + private class SimpleSocketCheck implements Closeable { + protected final InetAddress mTarget; + protected final int mAddressFamily; + protected final Measurement mMeasurement; + protected FileDescriptor mFileDescriptor; + protected SocketAddress mSocketAddress; + + protected SimpleSocketCheck(InetAddress target, Measurement measurement) { + mMeasurement = measurement; + + if (target instanceof Inet6Address) { + Inet6Address targetWithScopeId = null; + if (target.isLinkLocalAddress() && mInterfaceIndex != null) { + try { + targetWithScopeId = Inet6Address.getByAddress( + null, target.getAddress(), mInterfaceIndex); + } catch (UnknownHostException e) { + mMeasurement.recordFailure(e.toString()); + } + } + mTarget = (targetWithScopeId != null) ? targetWithScopeId : target; + mAddressFamily = AF_INET6; + } else { + mTarget = target; + mAddressFamily = AF_INET; + } + } + + protected void setupSocket( + int sockType, int protocol, long writeTimeout, long readTimeout, int dstPort) + throws ErrnoException, IOException { + mFileDescriptor = Os.socket(mAddressFamily, sockType, protocol); + // Setting SNDTIMEO is purely for defensive purposes. + Os.setsockoptTimeval(mFileDescriptor, + SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(writeTimeout)); + Os.setsockoptTimeval(mFileDescriptor, + SOL_SOCKET, SO_RCVTIMEO, StructTimeval.fromMillis(readTimeout)); + // TODO: Use IP_RECVERR/IPV6_RECVERR, pending OsContants availability. + mNetwork.bindSocket(mFileDescriptor); + Os.connect(mFileDescriptor, mTarget, dstPort); + mSocketAddress = Os.getsockname(mFileDescriptor); + } + + protected String getSocketAddressString() { + // The default toString() implementation is not the prettiest. + InetSocketAddress inetSockAddr = (InetSocketAddress) mSocketAddress; + InetAddress localAddr = inetSockAddr.getAddress(); + return String.format( + (localAddr instanceof Inet6Address ? "[%s]:%d" : "%s:%d"), + localAddr.getHostAddress(), inetSockAddr.getPort()); + } + + @Override + public void close() { + IoUtils.closeQuietly(mFileDescriptor); + } + } + + + private class IcmpCheck extends SimpleSocketCheck implements Runnable { + private static final int TIMEOUT_SEND = 100; + private static final int TIMEOUT_RECV = 300; + private static final int ICMPV4_ECHO_REQUEST = 8; + private static final int ICMPV6_ECHO_REQUEST = 128; + private static final int PACKET_BUFSIZE = 512; + private final int mProtocol; + private final int mIcmpType; + + public IcmpCheck(InetAddress target, Measurement measurement) { + super(target, measurement); + + if (mAddressFamily == AF_INET6) { + mProtocol = IPPROTO_ICMPV6; + mIcmpType = ICMPV6_ECHO_REQUEST; + mMeasurement.description = "ICMPv6"; + } else { + mProtocol = IPPROTO_ICMP; + mIcmpType = ICMPV4_ECHO_REQUEST; + mMeasurement.description = "ICMPv4"; + } + + mMeasurement.description += " dst{" + mTarget.getHostAddress() + "}"; + } + + @Override + public void run() { + // Check if this measurement has already failed during setup. + if (mMeasurement.finishTime > 0) { + // If the measurement failed during construction it didn't + // decrement the countdown latch; do so here. + mCountDownLatch.countDown(); + return; + } + + try { + setupSocket(SOCK_DGRAM, mProtocol, TIMEOUT_SEND, TIMEOUT_RECV, 0); + } catch (ErrnoException | IOException e) { + mMeasurement.recordFailure(e.toString()); + return; + } + mMeasurement.description += " src{" + getSocketAddressString() + "}"; + + // Build a trivial ICMP packet. + final byte[] icmpPacket = { + (byte) mIcmpType, 0, 0, 0, 0, 0, 0, 0 // ICMP header + }; + + int count = 0; + mMeasurement.startTime = now(); + while (now() < mDeadlineTime - (TIMEOUT_SEND + TIMEOUT_RECV)) { + count++; + icmpPacket[icmpPacket.length - 1] = (byte) count; + try { + Os.write(mFileDescriptor, icmpPacket, 0, icmpPacket.length); + } catch (ErrnoException | InterruptedIOException e) { + mMeasurement.recordFailure(e.toString()); + break; + } + + try { + ByteBuffer reply = ByteBuffer.allocate(PACKET_BUFSIZE); + Os.read(mFileDescriptor, reply); + // TODO: send a few pings back to back to guesstimate packet loss. + mMeasurement.recordSuccess("1/" + count); + break; + } catch (ErrnoException | InterruptedIOException e) { + continue; + } + } + if (mMeasurement.finishTime == 0) { + mMeasurement.recordFailure("0/" + count); + } + + close(); + } + } + + + private class DnsUdpCheck extends SimpleSocketCheck implements Runnable { + private static final int TIMEOUT_SEND = 100; + private static final int TIMEOUT_RECV = 500; + private static final int DNS_SERVER_PORT = 53; + private static final int RR_TYPE_A = 1; + private static final int RR_TYPE_AAAA = 28; + private static final int PACKET_BUFSIZE = 512; + + private final Random mRandom = new Random(); + + // Should be static, but the compiler mocks our puny, human attempts at reason. + private String responseCodeStr(int rcode) { + try { + return DnsResponseCode.values()[rcode].toString(); + } catch (IndexOutOfBoundsException e) { + return String.valueOf(rcode); + } + } + + private final int mQueryType; + + public DnsUdpCheck(InetAddress target, Measurement measurement) { + super(target, measurement); + + // TODO: Ideally, query the target for both types regardless of address family. + if (mAddressFamily == AF_INET6) { + mQueryType = RR_TYPE_AAAA; + } else { + mQueryType = RR_TYPE_A; + } + + mMeasurement.description = "DNS UDP dst{" + mTarget.getHostAddress() + "}"; + } + + @Override + public void run() { + // Check if this measurement has already failed during setup. + if (mMeasurement.finishTime > 0) { + // If the measurement failed during construction it didn't + // decrement the countdown latch; do so here. + mCountDownLatch.countDown(); + return; + } + + try { + setupSocket(SOCK_DGRAM, IPPROTO_UDP, TIMEOUT_SEND, TIMEOUT_RECV, DNS_SERVER_PORT); + } catch (ErrnoException | IOException e) { + mMeasurement.recordFailure(e.toString()); + return; + } + mMeasurement.description += " src{" + getSocketAddressString() + "}"; + + // This needs to be fixed length so it can be dropped into the pre-canned packet. + final String sixRandomDigits = + Integer.valueOf(mRandom.nextInt(900000) + 100000).toString(); + mMeasurement.description += " qtype{" + mQueryType + "}" + + " qname{" + sixRandomDigits + "-android-ds.metric.gstatic.com}"; + + // Build a trivial DNS packet. + final byte[] dnsPacket = getDnsQueryPacket(sixRandomDigits); + + int count = 0; + mMeasurement.startTime = now(); + while (now() < mDeadlineTime - (TIMEOUT_RECV + TIMEOUT_RECV)) { + count++; + try { + Os.write(mFileDescriptor, dnsPacket, 0, dnsPacket.length); + } catch (ErrnoException | InterruptedIOException e) { + mMeasurement.recordFailure(e.toString()); + break; + } + + try { + ByteBuffer reply = ByteBuffer.allocate(PACKET_BUFSIZE); + Os.read(mFileDescriptor, reply); + // TODO: more correct and detailed evaluation of the response, + // possibly adding the returned IP address(es) to the output. + final String rcodeStr = (reply.limit() > 3) + ? " " + responseCodeStr((int) (reply.get(3)) & 0x0f) + : ""; + mMeasurement.recordSuccess("1/" + count + rcodeStr); + break; + } catch (ErrnoException | InterruptedIOException e) { + continue; + } + } + if (mMeasurement.finishTime == 0) { + mMeasurement.recordFailure("0/" + count); + } + + close(); + } + + private byte[] getDnsQueryPacket(String sixRandomDigits) { + byte[] rnd = sixRandomDigits.getBytes(StandardCharsets.US_ASCII); + return new byte[] { + (byte) mRandom.nextInt(), (byte) mRandom.nextInt(), // [0-1] query ID + 1, 0, // [2-3] flags; byte[2] = 1 for recursion desired (RD). + 0, 1, // [4-5] QDCOUNT (number of queries) + 0, 0, // [6-7] ANCOUNT (number of answers) + 0, 0, // [8-9] NSCOUNT (number of name server records) + 0, 0, // [10-11] ARCOUNT (number of additional records) + 17, rnd[0], rnd[1], rnd[2], rnd[3], rnd[4], rnd[5], + '-', 'a', 'n', 'd', 'r', 'o', 'i', 'd', '-', 'd', 's', + 6, 'm', 'e', 't', 'r', 'i', 'c', + 7, 'g', 's', 't', 'a', 't', 'i', 'c', + 3, 'c', 'o', 'm', + 0, // null terminator of FQDN (root TLD) + 0, (byte) mQueryType, // QTYPE + 0, 1 // QCLASS, set to 1 = IN (Internet) + }; + } + } +}