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