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