content: /*
   * Copyright (C) 2015 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.server.connectivity;
  
  import static android.system.OsConstants.*;
  
  import android.net.LinkAddress;
  import android.net.LinkProperties;
  import android.net.Network;
  import android.net.NetworkUtils;
  import android.net.RouteInfo;
  import android.net.TrafficStats;
  import android.net.util.NetworkConstants;
  import android.os.SystemClock;
  import android.system.ErrnoException;
  import android.system.Os;
  import android.system.StructTimeval;
  import android.text.TextUtils;
  import android.util.Pair;
  
  import com.android.internal.util.IndentingPrintWriter;
  import com.android.internal.util.TrafficStatsConstants;
  
  import libcore.io.IoUtils;
  
  import java.io.Closeable;
  import java.io.FileDescriptor;
  import java.io.IOException;
  import java.io.InterruptedIOException;
  import java.net.Inet4Address;
  import java.net.Inet6Address;
  import java.net.InetAddress;
  import java.net.InetSocketAddress;
  import java.net.NetworkInterface;
  import java.net.SocketAddress;
  import java.net.SocketException;
  import java.net.UnknownHostException;
  import java.nio.ByteBuffer;
  import java.nio.charset.StandardCharsets;
  import java.util.ArrayList;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Random;
  import java.util.concurrent.CountDownLatch;
  import java.util.concurrent.TimeUnit;
  
  /**
   * NetworkDiagnostics
   *
   * A simple class to diagnose network connectivity fundamentals.  Current
   * checks performed are:
   *     - ICMPv4/v6 echo requests for all routers
   *     - ICMPv4/v6 echo requests for all DNS servers
   *     - DNS UDP queries to all DNS servers
   *
   * Currently unimplemented checks include:
   *     - report ARP/ND data about on-link neighbors
   *     - DNS TCP queries to all DNS servers
   *     - HTTP DIRECT and PROXY checks
   *     - port 443 blocking/TLS intercept checks
   *     - QUIC reachability checks
   *     - MTU checks
   *
   * The supplied timeout bounds the entire diagnostic process.  Each specific
   * check class must implement this upper bound on measurements in whichever
   * manner is most appropriate and effective.
   *
   * @hide
   */
  public class NetworkDiagnostics {
      private static final String TAG = "NetworkDiagnostics";
  
      private static final InetAddress TEST_DNS4 = NetworkUtils.numericToInetAddress("8.8.8.8");
      private static final InetAddress TEST_DNS6 = NetworkUtils.numericToInetAddress(
              "2001:4860:4860::8888");
  
      // For brevity elsewhere.
      private static final long now() {
          return SystemClock.elapsedRealtime();
      }
  
      // Values from RFC 1035 section 4.1.1, names from <arpa/nameser.h>.
      // Should be a member of DnsUdpCheck, but "compiler says no".
      public static enum DnsResponseCode { NOERROR, FORMERR, SERVFAIL, NXDOMAIN, NOTIMP, REFUSED };
  
      private final Network mNetwork;
      private final LinkProperties mLinkProperties;
      private final Integer mInterfaceIndex;
  
      private final long mTimeoutMs;
      private final long mStartTime;
      private final long mDeadlineTime;
  
      // A counter, initialized to the total number of measurements,
      // so callers can wait for completion.
      private final CountDownLatch mCountDownLatch;
  
      public class Measurement {
          private static final String SUCCEEDED = "SUCCEEDED";
          private static final String FAILED = "FAILED";
  
          private boolean succeeded;
  
          // Package private.  TODO: investigate better encapsulation.
          String description = "";
          long startTime;
          long finishTime;
          String result = "";
          Thread thread;
  
          public boolean checkSucceeded() { return succeeded; }
  
          void recordSuccess(String msg) {
              maybeFixupTimes();
              succeeded = true;
              result = SUCCEEDED + ": " + msg;
              if (mCountDownLatch != null) {
                  mCountDownLatch.countDown();
              }
          }
  
          void recordFailure(String msg) {
              maybeFixupTimes();
              succeeded = false;
              result = FAILED + ": " + msg;
              if (mCountDownLatch != null) {
                  mCountDownLatch.countDown();
              }
          }
  
          private void maybeFixupTimes() {
              // Allows the caller to just set success/failure and not worry
              // about also setting the correct finishing time.
              if (finishTime == 0) { finishTime = now(); }
  
              // In cases where, for example, a failure has occurred before the
              // measurement even began, fixup the start time to reflect as much.
              if (startTime == 0) { startTime = finishTime; }
          }
  
          @Override
          public String toString() {
              return description + ": " + result + " (" + (finishTime - startTime) + "ms)";
          }
      }
  
      private final Map<InetAddress, Measurement> mIcmpChecks = new HashMap<>();
      private final Map<Pair<InetAddress, InetAddress>, Measurement> mExplicitSourceIcmpChecks =
              new HashMap<>();
      private final Map<InetAddress, Measurement> mDnsUdpChecks = new HashMap<>();
      private final String mDescription;
  
  
      public NetworkDiagnostics(Network network, LinkProperties lp, long timeoutMs) {
          mNetwork = network;
          mLinkProperties = lp;
          mInterfaceIndex = getInterfaceIndex(mLinkProperties.getInterfaceName());
          mTimeoutMs = timeoutMs;
          mStartTime = now();
          mDeadlineTime = mStartTime + mTimeoutMs;
  
          // Hardcode measurements to TEST_DNS4 and TEST_DNS6 in order to test off-link connectivity.
          // We are free to modify mLinkProperties with impunity because ConnectivityService passes us
          // a copy and not the original object. It's easier to do it this way because we don't need
          // to check whether the LinkProperties already contains these DNS servers because
          // LinkProperties#addDnsServer checks for duplicates.
          if (mLinkProperties.isReachable(TEST_DNS4)) {
              mLinkProperties.addDnsServer(TEST_DNS4);
          }
          // TODO: we could use mLinkProperties.isReachable(TEST_DNS6) here, because we won't set any
          // DNS servers for which isReachable() is false, but since this is diagnostic code, be extra
          // careful.
          if (mLinkProperties.hasGlobalIpv6Address() || mLinkProperties.hasIpv6DefaultRoute()) {
              mLinkProperties.addDnsServer(TEST_DNS6);
          }
  
          for (RouteInfo route : mLinkProperties.getRoutes()) {
              if (route.hasGateway()) {
                  InetAddress gateway = route.getGateway();
                  prepareIcmpMeasurement(gateway);
                  if (route.isIPv6Default()) {
                      prepareExplicitSourceIcmpMeasurements(gateway);
                  }
              }
          }
          for (InetAddress nameserver : mLinkProperties.getDnsServers()) {
                  prepareIcmpMeasurement(nameserver);
                  prepareDnsMeasurement(nameserver);
          }
  
          mCountDownLatch = new CountDownLatch(totalMeasurementCount());
  
          startMeasurements();
  
          mDescription = "ifaces{" + TextUtils.join(",", mLinkProperties.getAllInterfaceNames()) + "}"
                  + " index{" + mInterfaceIndex + "}"
                  + " network{" + mNetwork + "}"
                  + " nethandle{" + mNetwork.getNetworkHandle() + "}";
      }
  
      private static Integer getInterfaceIndex(String ifname) {
          try {
              NetworkInterface ni = NetworkInterface.getByName(ifname);
              return ni.getIndex();
          } catch (NullPointerException | SocketException e) {
              return null;
          }
      }
  
      private void prepareIcmpMeasurement(InetAddress target) {
          if (!mIcmpChecks.containsKey(target)) {
              Measurement measurement = new Measurement();
              measurement.thread = new Thread(new IcmpCheck(target, measurement));
              mIcmpChecks.put(target, measurement);
          }
      }
  
      private void prepareExplicitSourceIcmpMeasurements(InetAddress target) {
          for (LinkAddress l : mLinkProperties.getLinkAddresses()) {
              InetAddress source = l.getAddress();
              if (source instanceof Inet6Address && l.isGlobalPreferred()) {
                  Pair<InetAddress, InetAddress> srcTarget = new Pair<>(source, target);
                  if (!mExplicitSourceIcmpChecks.containsKey(srcTarget)) {
                      Measurement measurement = new Measurement();
                      measurement.thread = new Thread(new IcmpCheck(source, target, measurement));
                      mExplicitSourceIcmpChecks.put(srcTarget, measurement);
                  }
              }
          }
      }
  
      private void prepareDnsMeasurement(InetAddress target) {
          if (!mDnsUdpChecks.containsKey(target)) {
              Measurement measurement = new Measurement();
              measurement.thread = new Thread(new DnsUdpCheck(target, measurement));
              mDnsUdpChecks.put(target, measurement);
          }
      }
  
      private int totalMeasurementCount() {
          return mIcmpChecks.size() + mExplicitSourceIcmpChecks.size() + mDnsUdpChecks.size();
      }
  
      private void startMeasurements() {
          for (Measurement measurement : mIcmpChecks.values()) {
              measurement.thread.start();
          }
          for (Measurement measurement : mExplicitSourceIcmpChecks.values()) {
              measurement.thread.start();
          }
          for (Measurement measurement : mDnsUdpChecks.values()) {
              measurement.thread.start();
          }
      }
  
      public void waitForMeasurements() {
          try {
              mCountDownLatch.await(mDeadlineTime - now(), TimeUnit.MILLISECONDS);
          } catch (InterruptedException ignored) {}
      }
  
      public List<Measurement> getMeasurements() {
          // TODO: Consider moving waitForMeasurements() in here to minimize the
          // chance of caller errors.
  
          ArrayList<Measurement> measurements = new ArrayList(totalMeasurementCount());
  
          // Sort measurements IPv4 first.
          for (Map.Entry<InetAddress, Measurement> entry : mIcmpChecks.entrySet()) {
              if (entry.getKey() instanceof Inet4Address) {
                  measurements.add(entry.getValue());
              }
          }
          for (Map.Entry<Pair<InetAddress, InetAddress>, Measurement> entry :
                  mExplicitSourceIcmpChecks.entrySet()) {
              if (entry.getKey().first instanceof Inet4Address) {
                  measurements.add(entry.getValue());
              }
          }
          for (Map.Entry<InetAddress, Measurement> entry : mDnsUdpChecks.entrySet()) {
              if (entry.getKey() instanceof Inet4Address) {
                  measurements.add(entry.getValue());
              }
          }
  
          // IPv6 measurements second.
          for (Map.Entry<InetAddress, Measurement> entry : mIcmpChecks.entrySet()) {
              if (entry.getKey() instanceof Inet6Address) {
                  measurements.add(entry.getValue());
              }
          }
          for (Map.Entry<Pair<InetAddress, InetAddress>, Measurement> entry :
                  mExplicitSourceIcmpChecks.entrySet()) {
              if (entry.getKey().first instanceof Inet6Address) {
                  measurements.add(entry.getValue());
              }
          }
          for (Map.Entry<InetAddress, Measurement> entry : mDnsUdpChecks.entrySet()) {
              if (entry.getKey() instanceof Inet6Address) {
                  measurements.add(entry.getValue());
              }
          }
  
          return measurements;
      }
  
      public void dump(IndentingPrintWriter pw) {
          pw.println(TAG + ":" + mDescription);
          final long unfinished = mCountDownLatch.getCount();
          if (unfinished > 0) {
              // This can't happen unless a caller forgets to call waitForMeasurements()
              // or a measurement isn't implemented to correctly honor the timeout.
              pw.println("WARNING: countdown wait incomplete: "
                      + unfinished + " unfinished measurements");
          }
  
          pw.increaseIndent();
  
          String prefix;
          for (Measurement m : getMeasurements()) {
              prefix = m.checkSucceeded() ? "." : "F";
              pw.println(prefix + "  " + m.toString());
          }
  
          pw.decreaseIndent();
      }
  
  
      private class SimpleSocketCheck implements Closeable {
          protected final InetAddress mSource;  // Usually null.
          protected final InetAddress mTarget;
          protected final int mAddressFamily;
          protected final Measurement mMeasurement;
          protected FileDescriptor mFileDescriptor;
          protected SocketAddress mSocketAddress;
  
          protected SimpleSocketCheck(
                  InetAddress source, InetAddress target, Measurement measurement) {
              mMeasurement = measurement;
  
              if (target instanceof Inet6Address) {
                  Inet6Address targetWithScopeId = null;
                  if (target.isLinkLocalAddress() && mInterfaceIndex != null) {
                      try {
                          targetWithScopeId = Inet6Address.getByAddress(
                                  null, target.getAddress(), mInterfaceIndex);
                      } catch (UnknownHostException e) {
                          mMeasurement.recordFailure(e.toString());
                      }
                  }
                  mTarget = (targetWithScopeId != null) ? targetWithScopeId : target;
                  mAddressFamily = AF_INET6;
              } else {
                  mTarget = target;
                  mAddressFamily = AF_INET;
              }
  
              // We don't need to check the scope ID here because we currently only do explicit-source
              // measurements from global IPv6 addresses.
              mSource = source;
          }
  
          protected SimpleSocketCheck(InetAddress target, Measurement measurement) {
              this(null, target, measurement);
          }
  
          protected void setupSocket(
                  int sockType, int protocol, long writeTimeout, long readTimeout, int dstPort)
                  throws ErrnoException, IOException {
              final int oldTag = TrafficStats.getAndSetThreadStatsTag(
                      TrafficStatsConstants.TAG_SYSTEM_PROBE);
              try {
                  mFileDescriptor = Os.socket(mAddressFamily, sockType, protocol);
              } finally {
                  TrafficStats.setThreadStatsTag(oldTag);
              }
              // Setting SNDTIMEO is purely for defensive purposes.
              Os.setsockoptTimeval(mFileDescriptor,
                      SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(writeTimeout));
              Os.setsockoptTimeval(mFileDescriptor,
                      SOL_SOCKET, SO_RCVTIMEO, StructTimeval.fromMillis(readTimeout));
              // TODO: Use IP_RECVERR/IPV6_RECVERR, pending OsContants availability.
              mNetwork.bindSocket(mFileDescriptor);
              if (mSource != null) {
                  Os.bind(mFileDescriptor, mSource, 0);
              }
              Os.connect(mFileDescriptor, mTarget, dstPort);
              mSocketAddress = Os.getsockname(mFileDescriptor);
          }
  
          protected String getSocketAddressString() {
              // The default toString() implementation is not the prettiest.
              InetSocketAddress inetSockAddr = (InetSocketAddress) mSocketAddress;
              InetAddress localAddr = inetSockAddr.getAddress();
              return String.format(
                      (localAddr instanceof Inet6Address ? "[%s]:%d" : "%s:%d"),
                      localAddr.getHostAddress(), inetSockAddr.getPort());
          }
  
          @Override
          public void close() {
              IoUtils.closeQuietly(mFileDescriptor);
          }
      }
  
  
      private class IcmpCheck extends SimpleSocketCheck implements Runnable {
          private static final int TIMEOUT_SEND = 100;
          private static final int TIMEOUT_RECV = 300;
          private static final int PACKET_BUFSIZE = 512;
          private final int mProtocol;
          private final int mIcmpType;
  
          public IcmpCheck(InetAddress source, InetAddress target, Measurement measurement) {
              super(source, target, measurement);
  
              if (mAddressFamily == AF_INET6) {
                  mProtocol = IPPROTO_ICMPV6;
                  mIcmpType = NetworkConstants.ICMPV6_ECHO_REQUEST_TYPE;
                  mMeasurement.description = "ICMPv6";
              } else {
                  mProtocol = IPPROTO_ICMP;
                  mIcmpType = NetworkConstants.ICMPV4_ECHO_REQUEST_TYPE;
                  mMeasurement.description = "ICMPv4";
              }
  
              mMeasurement.description += " dst{" + mTarget.getHostAddress() + "}";
          }
  
          public IcmpCheck(InetAddress target, Measurement measurement) {
              this(null, target, measurement);
          }
  
          @Override
          public void run() {
              // Check if this measurement has already failed during setup.
              if (mMeasurement.finishTime > 0) {
                  // If the measurement failed during construction it didn't
                  // decrement the countdown latch; do so here.
                  mCountDownLatch.countDown();
                  return;
              }
  
              try {
                  setupSocket(SOCK_DGRAM, mProtocol, TIMEOUT_SEND, TIMEOUT_RECV, 0);
              } catch (ErrnoException | IOException e) {
                  mMeasurement.recordFailure(e.toString());
                  return;
              }
              mMeasurement.description += " src{" + getSocketAddressString() + "}";
  
              // Build a trivial ICMP packet.
              final byte[] icmpPacket = {
                      (byte) mIcmpType, 0, 0, 0, 0, 0, 0, 0  // ICMP header
              };
  
              int count = 0;
              mMeasurement.startTime = now();
              while (now() < mDeadlineTime - (TIMEOUT_SEND + TIMEOUT_RECV)) {
                  count++;
                  icmpPacket[icmpPacket.length - 1] = (byte) count;
                  try {
                      Os.write(mFileDescriptor, icmpPacket, 0, icmpPacket.length);
                  } catch (ErrnoException | InterruptedIOException e) {
                      mMeasurement.recordFailure(e.toString());
                      break;
                  }
  
                  try {
                      ByteBuffer reply = ByteBuffer.allocate(PACKET_BUFSIZE);
                      Os.read(mFileDescriptor, reply);
                      // TODO: send a few pings back to back to guesstimate packet loss.
                      mMeasurement.recordSuccess("1/" + count);
                      break;
                  } catch (ErrnoException | InterruptedIOException e) {
                      continue;
                  }
              }
              if (mMeasurement.finishTime == 0) {
                  mMeasurement.recordFailure("0/" + count);
              }
  
              close();
          }
      }
  
  
      private class DnsUdpCheck extends SimpleSocketCheck implements Runnable {
          private static final int TIMEOUT_SEND = 100;
          private static final int TIMEOUT_RECV = 500;
          private static final int RR_TYPE_A = 1;
          private static final int RR_TYPE_AAAA = 28;
          private static final int PACKET_BUFSIZE = 512;
  
          private final Random mRandom = new Random();
  
          // Should be static, but the compiler mocks our puny, human attempts at reason.
          private String responseCodeStr(int rcode) {
              try {
                  return DnsResponseCode.values()[rcode].toString();
              } catch (IndexOutOfBoundsException e) {
                  return String.valueOf(rcode);
              }
          }
  
          private final int mQueryType;
  
          public DnsUdpCheck(InetAddress target, Measurement measurement) {
              super(target, measurement);
  
              // TODO: Ideally, query the target for both types regardless of address family.
              if (mAddressFamily == AF_INET6) {
                  mQueryType = RR_TYPE_AAAA;
              } else {
                  mQueryType = RR_TYPE_A;
              }
  
              mMeasurement.description = "DNS UDP dst{" + mTarget.getHostAddress() + "}";
          }
  
          @Override
          public void run() {
              // Check if this measurement has already failed during setup.
              if (mMeasurement.finishTime > 0) {
                  // If the measurement failed during construction it didn't
                  // decrement the countdown latch; do so here.
                  mCountDownLatch.countDown();
                  return;
              }
  
              try {
                  setupSocket(SOCK_DGRAM, IPPROTO_UDP, TIMEOUT_SEND, TIMEOUT_RECV,
                          NetworkConstants.DNS_SERVER_PORT);
              } catch (ErrnoException | IOException e) {
                  mMeasurement.recordFailure(e.toString());
                  return;
              }
              mMeasurement.description += " src{" + getSocketAddressString() + "}";
  
              // This needs to be fixed length so it can be dropped into the pre-canned packet.
              final String sixRandomDigits = String.valueOf(mRandom.nextInt(900000) + 100000);
              mMeasurement.description += " qtype{" + mQueryType + "}"
                      + " qname{" + sixRandomDigits + "-android-ds.metric.gstatic.com}";
  
              // Build a trivial DNS packet.
              final byte[] dnsPacket = getDnsQueryPacket(sixRandomDigits);
  
              int count = 0;
              mMeasurement.startTime = now();
              while (now() < mDeadlineTime - (TIMEOUT_RECV + TIMEOUT_RECV)) {
                  count++;
                  try {
                      Os.write(mFileDescriptor, dnsPacket, 0, dnsPacket.length);
                  } catch (ErrnoException | InterruptedIOException e) {
                      mMeasurement.recordFailure(e.toString());
                      break;
                  }
  
                  try {
                      ByteBuffer reply = ByteBuffer.allocate(PACKET_BUFSIZE);
                      Os.read(mFileDescriptor, reply);
                      // TODO: more correct and detailed evaluation of the response,
                      // possibly adding the returned IP address(es) to the output.
                      final String rcodeStr = (reply.limit() > 3)
                              ? " " + responseCodeStr((int) (reply.get(3)) & 0x0f)
                              : "";
                      mMeasurement.recordSuccess("1/" + count + rcodeStr);
                      break;
                  } catch (ErrnoException | InterruptedIOException e) {
                      continue;
                  }
              }
              if (mMeasurement.finishTime == 0) {
                  mMeasurement.recordFailure("0/" + count);
              }
  
              close();
          }
  
          private byte[] getDnsQueryPacket(String sixRandomDigits) {
              byte[] rnd = sixRandomDigits.getBytes(StandardCharsets.US_ASCII);
              return new byte[] {
                  (byte) mRandom.nextInt(), (byte) mRandom.nextInt(),  // [0-1]   query ID
                  1, 0,  // [2-3]   flags; byte[2] = 1 for recursion desired (RD).
                  0, 1,  // [4-5]   QDCOUNT (number of queries)
                  0, 0,  // [6-7]   ANCOUNT (number of answers)
                  0, 0,  // [8-9]   NSCOUNT (number of name server records)
                  0, 0,  // [10-11] ARCOUNT (number of additional records)
                  17, rnd[0], rnd[1], rnd[2], rnd[3], rnd[4], rnd[5],
                          '-', 'a', 'n', 'd', 'r', 'o', 'i', 'd', '-', 'd', 's',
                  6, 'm', 'e', 't', 'r', 'i', 'c',
                  7, 'g', 's', 't', 'a', 't', 'i', 'c',
                  3, 'c', 'o', 'm',
                  0,  // null terminator of FQDN (root TLD)
                  0, (byte) mQueryType,  // QTYPE
                  0, 1  // QCLASS, set to 1 = IN (Internet)
              };
          }
      }
  }
  