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