content: /*
   * Copyright (C) 2016 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.util.TimeUtils.NANOS_PER_MS;
  
  import android.content.Context;
  import android.net.ConnectivityManager;
  import android.net.INetdEventCallback;
  import android.net.MacAddress;
  import android.net.Network;
  import android.net.NetworkCapabilities;
  import android.net.metrics.ConnectStats;
  import android.net.metrics.DnsEvent;
  import android.net.metrics.INetdEventListener;
  import android.net.metrics.NetworkMetrics;
  import android.net.metrics.WakeupEvent;
  import android.net.metrics.WakeupStats;
  import android.os.RemoteException;
  import android.text.format.DateUtils;
  import android.util.ArrayMap;
  import android.util.Log;
  import android.util.SparseArray;
  import android.util.StatsLog;
  
  import com.android.internal.annotations.GuardedBy;
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.util.BitUtils;
  import com.android.internal.util.RingBuffer;
  import com.android.internal.util.TokenBucket;
  import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityEvent;
  
  import java.io.PrintWriter;
  import java.util.ArrayList;
  import java.util.List;
  import java.util.StringJoiner;
  
  /**
   * Implementation of the INetdEventListener interface.
   */
  public class NetdEventListenerService extends INetdEventListener.Stub {
  
      public static final String SERVICE_NAME = "netd_listener";
  
      private static final String TAG = NetdEventListenerService.class.getSimpleName();
      private static final boolean DBG = false;
  
      // Rate limit connect latency logging to 1 measurement per 15 seconds (5760 / day) with maximum
      // bursts of 5000 measurements.
      private static final int CONNECT_LATENCY_BURST_LIMIT  = 5000;
      private static final int CONNECT_LATENCY_FILL_RATE    = 15 * (int) DateUtils.SECOND_IN_MILLIS;
  
      private static final long METRICS_SNAPSHOT_SPAN_MS = 5 * DateUtils.MINUTE_IN_MILLIS;
      private static final int METRICS_SNAPSHOT_BUFFER_SIZE = 48; // 4 hours
  
      @VisibleForTesting
      static final int WAKEUP_EVENT_BUFFER_LENGTH = 1024;
      // TODO: dedup this String constant with the one used in
      // ConnectivityService#wakeupModifyInterface().
      @VisibleForTesting
      static final String WAKEUP_EVENT_IFACE_PREFIX = "iface:";
  
      // Array of aggregated DNS and connect events sent by netd, grouped by net id.
      @GuardedBy("this")
      private final SparseArray<NetworkMetrics> mNetworkMetrics = new SparseArray<>();
  
      @GuardedBy("this")
      private final RingBuffer<NetworkMetricsSnapshot> mNetworkMetricsSnapshots =
              new RingBuffer<>(NetworkMetricsSnapshot.class, METRICS_SNAPSHOT_BUFFER_SIZE);
      @GuardedBy("this")
      private long mLastSnapshot = 0;
  
      // Array of aggregated wakeup event stats, grouped by interface name.
      @GuardedBy("this")
      private final ArrayMap<String, WakeupStats> mWakeupStats = new ArrayMap<>();
      // Ring buffer array for storing packet wake up events sent by Netd.
      @GuardedBy("this")
      private final RingBuffer<WakeupEvent> mWakeupEvents =
              new RingBuffer<>(WakeupEvent.class, WAKEUP_EVENT_BUFFER_LENGTH);
  
      private final ConnectivityManager mCm;
  
      @GuardedBy("this")
      private final TokenBucket mConnectTb =
              new TokenBucket(CONNECT_LATENCY_FILL_RATE, CONNECT_LATENCY_BURST_LIMIT);
  
  
      /**
       * There are only 3 possible callbacks.
       *
       * mNetdEventCallbackList[CALLBACK_CALLER_CONNECTIVITY_SERVICE]
       * Callback registered/unregistered by ConnectivityService.
       *
       * mNetdEventCallbackList[CALLBACK_CALLER_DEVICE_POLICY]
       * Callback registered/unregistered when logging is being enabled/disabled in DPM
       * by the device owner. It's DevicePolicyManager's responsibility to ensure that.
       *
       * mNetdEventCallbackList[CALLBACK_CALLER_NETWORK_WATCHLIST]
       * Callback registered/unregistered by NetworkWatchlistService.
       */
      @GuardedBy("this")
      private static final int[] ALLOWED_CALLBACK_TYPES = {
          INetdEventCallback.CALLBACK_CALLER_CONNECTIVITY_SERVICE,
          INetdEventCallback.CALLBACK_CALLER_DEVICE_POLICY,
          INetdEventCallback.CALLBACK_CALLER_NETWORK_WATCHLIST
      };
  
      @GuardedBy("this")
      private INetdEventCallback[] mNetdEventCallbackList =
              new INetdEventCallback[ALLOWED_CALLBACK_TYPES.length];
  
      public synchronized boolean addNetdEventCallback(int callerType, INetdEventCallback callback) {
          if (!isValidCallerType(callerType)) {
              Log.e(TAG, "Invalid caller type: " + callerType);
              return false;
          }
          mNetdEventCallbackList[callerType] = callback;
          return true;
      }
  
      public synchronized boolean removeNetdEventCallback(int callerType) {
          if (!isValidCallerType(callerType)) {
              Log.e(TAG, "Invalid caller type: " + callerType);
              return false;
          }
          mNetdEventCallbackList[callerType] = null;
          return true;
      }
  
      private static boolean isValidCallerType(int callerType) {
          for (int i = 0; i < ALLOWED_CALLBACK_TYPES.length; i++) {
              if (callerType == ALLOWED_CALLBACK_TYPES[i]) {
                  return true;
              }
          }
          return false;
      }
  
      public NetdEventListenerService(Context context) {
          this(context.getSystemService(ConnectivityManager.class));
      }
  
      @VisibleForTesting
      public NetdEventListenerService(ConnectivityManager cm) {
          // We are started when boot is complete, so ConnectivityService should already be running.
          mCm = cm;
      }
  
      private static long projectSnapshotTime(long timeMs) {
          return (timeMs / METRICS_SNAPSHOT_SPAN_MS) * METRICS_SNAPSHOT_SPAN_MS;
      }
  
      private NetworkMetrics getMetricsForNetwork(long timeMs, int netId) {
          collectPendingMetricsSnapshot(timeMs);
          NetworkMetrics metrics = mNetworkMetrics.get(netId);
          if (metrics == null) {
              // TODO: allow to change transport for a given netid.
              metrics = new NetworkMetrics(netId, getTransports(netId), mConnectTb);
              mNetworkMetrics.put(netId, metrics);
          }
          return metrics;
      }
  
      private NetworkMetricsSnapshot[] getNetworkMetricsSnapshots() {
          collectPendingMetricsSnapshot(System.currentTimeMillis());
          return mNetworkMetricsSnapshots.toArray();
      }
  
      private void collectPendingMetricsSnapshot(long timeMs) {
          // Detects time differences larger than the snapshot collection period.
          // This is robust against clock jumps and long inactivity periods.
          if (Math.abs(timeMs - mLastSnapshot) <= METRICS_SNAPSHOT_SPAN_MS) {
              return;
          }
          mLastSnapshot = projectSnapshotTime(timeMs);
          NetworkMetricsSnapshot snapshot =
                  NetworkMetricsSnapshot.collect(mLastSnapshot, mNetworkMetrics);
          if (snapshot.stats.isEmpty()) {
              return;
          }
          mNetworkMetricsSnapshots.append(snapshot);
      }
  
      @Override
      // Called concurrently by multiple binder threads.
      // This method must not block or perform long-running operations.
      public synchronized void onDnsEvent(int netId, int eventType, int returnCode, int latencyMs,
              String hostname, String[] ipAddresses, int ipAddressesCount, int uid)
              throws RemoteException {
          long timestamp = System.currentTimeMillis();
          getMetricsForNetwork(timestamp, netId).addDnsResult(eventType, returnCode, latencyMs);
  
          for (INetdEventCallback callback : mNetdEventCallbackList) {
              if (callback != null) {
                  callback.onDnsEvent(netId, eventType, returnCode, hostname, ipAddresses,
                          ipAddressesCount, timestamp, uid);
              }
          }
      }
  
      @Override
      // Called concurrently by multiple binder threads.
      // This method must not block or perform long-running operations.
      public synchronized void onNat64PrefixEvent(int netId,
              boolean added, String prefixString, int prefixLength)
              throws RemoteException {
          for (INetdEventCallback callback : mNetdEventCallbackList) {
              if (callback != null) {
                  callback.onNat64PrefixEvent(netId, added, prefixString, prefixLength);
              }
          }
      }
  
      @Override
      // Called concurrently by multiple binder threads.
      // This method must not block or perform long-running operations.
      public synchronized void onPrivateDnsValidationEvent(int netId,
              String ipAddress, String hostname, boolean validated)
              throws RemoteException {
          for (INetdEventCallback callback : mNetdEventCallbackList) {
              if (callback != null) {
                  callback.onPrivateDnsValidationEvent(netId, ipAddress, hostname, validated);
              }
          }
      }
  
      @Override
      // Called concurrently by multiple binder threads.
      // This method must not block or perform long-running operations.
      public synchronized void onConnectEvent(int netId, int error, int latencyMs, String ipAddr,
              int port, int uid) throws RemoteException {
          long timestamp = System.currentTimeMillis();
          getMetricsForNetwork(timestamp, netId).addConnectResult(error, latencyMs, ipAddr);
  
          for (INetdEventCallback callback : mNetdEventCallbackList) {
              if (callback != null) {
                  callback.onConnectEvent(ipAddr, port, timestamp, uid);
              }
          }
      }
  
      @Override
      public synchronized void onWakeupEvent(String prefix, int uid, int ethertype, int ipNextHeader,
              byte[] dstHw, String srcIp, String dstIp, int srcPort, int dstPort, long timestampNs) {
          String iface = prefix.replaceFirst(WAKEUP_EVENT_IFACE_PREFIX, "");
          final long timestampMs;
          if (timestampNs > 0) {
              timestampMs = timestampNs / NANOS_PER_MS;
          } else {
              timestampMs = System.currentTimeMillis();
          }
  
          WakeupEvent event = new WakeupEvent();
          event.iface = iface;
          event.timestampMs = timestampMs;
          event.uid = uid;
          event.ethertype = ethertype;
          event.dstHwAddr = MacAddress.fromBytes(dstHw);
          event.srcIp = srcIp;
          event.dstIp = dstIp;
          event.ipNextHeader = ipNextHeader;
          event.srcPort = srcPort;
          event.dstPort = dstPort;
          addWakeupEvent(event);
  
          String dstMac = event.dstHwAddr.toString();
          StatsLog.write(StatsLog.PACKET_WAKEUP_OCCURRED,
                  uid, iface, ethertype, dstMac, srcIp, dstIp, ipNextHeader, srcPort, dstPort);
      }
  
      @Override
      public synchronized void onTcpSocketStatsEvent(int[] networkIds,
              int[] sentPackets, int[] lostPackets, int[] rttsUs, int[] sentAckDiffsMs) {
          if (networkIds.length != sentPackets.length
                  || networkIds.length != lostPackets.length
                  || networkIds.length != rttsUs.length
                  || networkIds.length != sentAckDiffsMs.length) {
              Log.e(TAG, "Mismatched lengths of TCP socket stats data arrays");
              return;
          }
  
          long timestamp = System.currentTimeMillis();
          for (int i = 0; i < networkIds.length; i++) {
              int netId = networkIds[i];
              int sent = sentPackets[i];
              int lost = lostPackets[i];
              int rttUs = rttsUs[i];
              int sentAckDiffMs = sentAckDiffsMs[i];
              getMetricsForNetwork(timestamp, netId)
                      .addTcpStatsResult(sent, lost, rttUs, sentAckDiffMs);
          }
      }
  
      @Override
      public int getInterfaceVersion() throws RemoteException {
          return this.VERSION;
      }
  
      private void addWakeupEvent(WakeupEvent event) {
          String iface = event.iface;
          mWakeupEvents.append(event);
          WakeupStats stats = mWakeupStats.get(iface);
          if (stats == null) {
              stats = new WakeupStats(iface);
              mWakeupStats.put(iface, stats);
          }
          stats.countEvent(event);
      }
  
      public synchronized void flushStatistics(List<IpConnectivityEvent> events) {
          for (int i = 0; i < mNetworkMetrics.size(); i++) {
              ConnectStats stats = mNetworkMetrics.valueAt(i).connectMetrics;
              if (stats.eventCount == 0) {
                  continue;
              }
              events.add(IpConnectivityEventBuilder.toProto(stats));
          }
          for (int i = 0; i < mNetworkMetrics.size(); i++) {
              DnsEvent ev = mNetworkMetrics.valueAt(i).dnsMetrics;
              if (ev.eventCount == 0) {
                  continue;
              }
              events.add(IpConnectivityEventBuilder.toProto(ev));
          }
          for (int i = 0; i < mWakeupStats.size(); i++) {
              events.add(IpConnectivityEventBuilder.toProto(mWakeupStats.valueAt(i)));
          }
          mNetworkMetrics.clear();
          mWakeupStats.clear();
      }
  
      public synchronized void list(PrintWriter pw) {
          pw.println("dns/connect events:");
          for (int i = 0; i < mNetworkMetrics.size(); i++) {
              pw.println(mNetworkMetrics.valueAt(i).connectMetrics);
          }
          for (int i = 0; i < mNetworkMetrics.size(); i++) {
              pw.println(mNetworkMetrics.valueAt(i).dnsMetrics);
          }
          pw.println("");
          pw.println("network statistics:");
          for (NetworkMetricsSnapshot s : getNetworkMetricsSnapshots()) {
              pw.println(s);
          }
          pw.println("");
          pw.println("packet wakeup events:");
          for (int i = 0; i < mWakeupStats.size(); i++) {
              pw.println(mWakeupStats.valueAt(i));
          }
          for (WakeupEvent wakeup : mWakeupEvents.toArray()) {
              pw.println(wakeup);
          }
      }
  
      public synchronized void listAsProtos(PrintWriter pw) {
          for (int i = 0; i < mNetworkMetrics.size(); i++) {
              pw.print(IpConnectivityEventBuilder.toProto(mNetworkMetrics.valueAt(i).connectMetrics));
          }
          for (int i = 0; i < mNetworkMetrics.size(); i++) {
              pw.print(IpConnectivityEventBuilder.toProto(mNetworkMetrics.valueAt(i).dnsMetrics));
          }
          for (int i = 0; i < mWakeupStats.size(); i++) {
              pw.print(IpConnectivityEventBuilder.toProto(mWakeupStats.valueAt(i)));
          }
      }
  
      private long getTransports(int netId) {
          // TODO: directly query ConnectivityService instead of going through Binder interface.
          NetworkCapabilities nc = mCm.getNetworkCapabilities(new Network(netId));
          if (nc == null) {
              return 0;
          }
          return BitUtils.packBits(nc.getTransportTypes());
      }
  
      private static void maybeLog(String s, Object... args) {
          if (DBG) Log.d(TAG, String.format(s, args));
      }
  
      /** Helper class for buffering summaries of NetworkMetrics at regular time intervals */
      static class NetworkMetricsSnapshot {
  
          public long timeMs;
          public List<NetworkMetrics.Summary> stats = new ArrayList<>();
  
          static NetworkMetricsSnapshot collect(long timeMs, SparseArray<NetworkMetrics> networkMetrics) {
              NetworkMetricsSnapshot snapshot = new NetworkMetricsSnapshot();
              snapshot.timeMs = timeMs;
              for (int i = 0; i < networkMetrics.size(); i++) {
                  NetworkMetrics.Summary s = networkMetrics.valueAt(i).getPendingStats();
                  if (s != null) {
                      snapshot.stats.add(s);
                  }
              }
              return snapshot;
          }
  
          @Override
          public String toString() {
              StringJoiner j = new StringJoiner(", ");
              for (NetworkMetrics.Summary s : stats) {
                  j.add(s.toString());
              }
              return String.format("%tT.%tL: %s", timeMs, timeMs, j.toString());
          }
      }
  }
  