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