Home | History | Annotate | Download | only in connectivity
      1 /*
      2  * Copyright (C) 2016 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 
     17 package com.android.server.connectivity;
     18 
     19 import android.content.Context;
     20 import android.net.ConnectivityMetricsEvent;
     21 import android.net.IIpConnectivityMetrics;
     22 import android.net.INetdEventCallback;
     23 import android.net.metrics.ApfProgramEvent;
     24 import android.net.metrics.IpConnectivityLog;
     25 import android.os.Binder;
     26 import android.os.IBinder;
     27 import android.os.Parcelable;
     28 import android.os.Process;
     29 import android.provider.Settings;
     30 import android.text.TextUtils;
     31 import android.text.format.DateUtils;
     32 import android.util.ArrayMap;
     33 import android.util.Base64;
     34 import android.util.Log;
     35 import com.android.internal.annotations.GuardedBy;
     36 import com.android.internal.annotations.VisibleForTesting;
     37 import com.android.internal.util.TokenBucket;
     38 import com.android.server.SystemService;
     39 import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityEvent;
     40 import java.io.FileDescriptor;
     41 import java.io.IOException;
     42 import java.io.PrintWriter;
     43 import java.util.ArrayList;
     44 import java.util.List;
     45 import java.util.function.ToIntFunction;
     46 
     47 /** {@hide} */
     48 final public class IpConnectivityMetrics extends SystemService {
     49     private static final String TAG = IpConnectivityMetrics.class.getSimpleName();
     50     private static final boolean DBG = false;
     51 
     52     // The logical version numbers of ipconnectivity.proto, corresponding to the
     53     // "version" field of IpConnectivityLog.
     54     private static final int NYC      = 0;
     55     private static final int NYC_MR1  = 1;
     56     private static final int NYC_MR2  = 2;
     57     public static final int VERSION   = NYC_MR2;
     58 
     59     private static final String SERVICE_NAME = IpConnectivityLog.SERVICE_NAME;
     60 
     61     // Default size of the event buffer. Once the buffer is full, incoming events are dropped.
     62     private static final int DEFAULT_BUFFER_SIZE = 2000;
     63     // Maximum size of the event buffer.
     64     private static final int MAXIMUM_BUFFER_SIZE = DEFAULT_BUFFER_SIZE * 10;
     65 
     66     private static final int MAXIMUM_CONNECT_LATENCY_RECORDS = 20000;
     67 
     68     private static final int ERROR_RATE_LIMITED = -1;
     69 
     70     // Lock ensuring that concurrent manipulations of the event buffer are correct.
     71     // There are three concurrent operations to synchronize:
     72     //  - appending events to the buffer.
     73     //  - iterating throught the buffer.
     74     //  - flushing the buffer content and replacing it by a new buffer.
     75     private final Object mLock = new Object();
     76 
     77     @VisibleForTesting
     78     public final Impl impl = new Impl();
     79     @VisibleForTesting
     80     NetdEventListenerService mNetdListener;
     81 
     82     @GuardedBy("mLock")
     83     private ArrayList<ConnectivityMetricsEvent> mBuffer;
     84     @GuardedBy("mLock")
     85     private int mDropped;
     86     @GuardedBy("mLock")
     87     private int mCapacity;
     88     @GuardedBy("mLock")
     89     private final ArrayMap<Class<?>, TokenBucket> mBuckets = makeRateLimitingBuckets();
     90 
     91     private final ToIntFunction<Context> mCapacityGetter;
     92 
     93     public IpConnectivityMetrics(Context ctx, ToIntFunction<Context> capacityGetter) {
     94         super(ctx);
     95         mCapacityGetter = capacityGetter;
     96         initBuffer();
     97     }
     98 
     99     public IpConnectivityMetrics(Context ctx) {
    100         this(ctx, READ_BUFFER_SIZE);
    101     }
    102 
    103     @Override
    104     public void onStart() {
    105         if (DBG) Log.d(TAG, "onStart");
    106     }
    107 
    108     @Override
    109     public void onBootPhase(int phase) {
    110         if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
    111             if (DBG) Log.d(TAG, "onBootPhase");
    112             mNetdListener = new NetdEventListenerService(getContext());
    113 
    114             publishBinderService(SERVICE_NAME, impl);
    115             publishBinderService(mNetdListener.SERVICE_NAME, mNetdListener);
    116         }
    117     }
    118 
    119     @VisibleForTesting
    120     public int bufferCapacity() {
    121         return mCapacityGetter.applyAsInt(getContext());
    122     }
    123 
    124     private void initBuffer() {
    125         synchronized (mLock) {
    126             mDropped = 0;
    127             mCapacity = bufferCapacity();
    128             mBuffer = new ArrayList<>(mCapacity);
    129         }
    130     }
    131 
    132     private int append(ConnectivityMetricsEvent event) {
    133         if (DBG) Log.d(TAG, "logEvent: " + event);
    134         synchronized (mLock) {
    135             final int left = mCapacity - mBuffer.size();
    136             if (event == null) {
    137                 return left;
    138             }
    139             if (isRateLimited(event)) {
    140                 // Do not count as a dropped event. TODO: consider adding separate counter
    141                 return ERROR_RATE_LIMITED;
    142             }
    143             if (left == 0) {
    144                 mDropped++;
    145                 return 0;
    146             }
    147             mBuffer.add(event);
    148             return left - 1;
    149         }
    150     }
    151 
    152     private boolean isRateLimited(ConnectivityMetricsEvent event) {
    153         TokenBucket tb = mBuckets.get(event.data.getClass());
    154         return (tb != null) && !tb.get();
    155     }
    156 
    157     private String flushEncodedOutput() {
    158         final ArrayList<ConnectivityMetricsEvent> events;
    159         final int dropped;
    160         synchronized (mLock) {
    161             events = mBuffer;
    162             dropped = mDropped;
    163             initBuffer();
    164         }
    165 
    166         final List<IpConnectivityEvent> protoEvents = IpConnectivityEventBuilder.toProto(events);
    167 
    168         if (mNetdListener != null) {
    169             mNetdListener.flushStatistics(protoEvents);
    170         }
    171 
    172         final byte[] data;
    173         try {
    174             data = IpConnectivityEventBuilder.serialize(dropped, protoEvents);
    175         } catch (IOException e) {
    176             Log.e(TAG, "could not serialize events", e);
    177             return "";
    178         }
    179 
    180         return Base64.encodeToString(data, Base64.DEFAULT);
    181     }
    182 
    183     /**
    184      * Clears the event buffer and prints its content as a protobuf serialized byte array
    185      * inside a base64 encoded string.
    186      */
    187     private void cmdFlush(FileDescriptor fd, PrintWriter pw, String[] args) {
    188         pw.print(flushEncodedOutput());
    189     }
    190 
    191     /**
    192      * Prints the content of the event buffer, either using the events ASCII representation
    193      * or using protobuf text format.
    194      */
    195     private void cmdList(FileDescriptor fd, PrintWriter pw, String[] args) {
    196         final ArrayList<ConnectivityMetricsEvent> events;
    197         synchronized (mLock) {
    198             events = new ArrayList(mBuffer);
    199         }
    200 
    201         if (args.length > 1 && args[1].equals("proto")) {
    202             for (IpConnectivityEvent ev : IpConnectivityEventBuilder.toProto(events)) {
    203                 pw.print(ev.toString());
    204             }
    205             if (mNetdListener != null) {
    206                 mNetdListener.listAsProtos(pw);
    207             }
    208             return;
    209         }
    210 
    211         for (ConnectivityMetricsEvent ev : events) {
    212             pw.println(ev.toString());
    213         }
    214         if (mNetdListener != null) {
    215             mNetdListener.list(pw);
    216         }
    217     }
    218 
    219     private void cmdStats(FileDescriptor fd, PrintWriter pw, String[] args) {
    220         synchronized (mLock) {
    221             pw.println("Buffered events: " + mBuffer.size());
    222             pw.println("Buffer capacity: " + mCapacity);
    223             pw.println("Dropped events: " + mDropped);
    224         }
    225         if (mNetdListener != null) {
    226             mNetdListener.dump(pw);
    227         }
    228     }
    229 
    230     private void cmdDefault(FileDescriptor fd, PrintWriter pw, String[] args) {
    231         if (args.length == 0) {
    232             pw.println("No command");
    233             return;
    234         }
    235         pw.println("Unknown command " + TextUtils.join(" ", args));
    236     }
    237 
    238     public final class Impl extends IIpConnectivityMetrics.Stub {
    239         static final String CMD_FLUSH   = "flush";
    240         static final String CMD_LIST    = "list";
    241         static final String CMD_STATS   = "stats";
    242         static final String CMD_DUMPSYS = "-a"; // dumpsys.cpp dumps services with "-a" as arguments
    243         static final String CMD_DEFAULT = CMD_STATS;
    244 
    245         @Override
    246         public int logEvent(ConnectivityMetricsEvent event) {
    247             enforceConnectivityInternalPermission();
    248             return append(event);
    249         }
    250 
    251         @Override
    252         public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
    253             enforceDumpPermission();
    254             if (DBG) Log.d(TAG, "dumpsys " + TextUtils.join(" ", args));
    255             final String cmd = (args.length > 0) ? args[0] : CMD_DEFAULT;
    256             switch (cmd) {
    257                 case CMD_FLUSH:
    258                     cmdFlush(fd, pw, args);
    259                     return;
    260                 case CMD_DUMPSYS:
    261                     // Fallthrough to CMD_LIST when dumpsys.cpp dumps services states (bug reports)
    262                 case CMD_LIST:
    263                     cmdList(fd, pw, args);
    264                     return;
    265                 case CMD_STATS:
    266                     cmdStats(fd, pw, args);
    267                     return;
    268                 default:
    269                     cmdDefault(fd, pw, args);
    270             }
    271         }
    272 
    273         private void enforceConnectivityInternalPermission() {
    274             enforcePermission(android.Manifest.permission.CONNECTIVITY_INTERNAL);
    275         }
    276 
    277         private void enforceDumpPermission() {
    278             enforcePermission(android.Manifest.permission.DUMP);
    279         }
    280 
    281         private void enforcePermission(String what) {
    282             getContext().enforceCallingOrSelfPermission(what, "IpConnectivityMetrics");
    283         }
    284 
    285         private void enforceNetdEventListeningPermission() {
    286             final int uid = Binder.getCallingUid();
    287             if (uid != Process.SYSTEM_UID) {
    288                 throw new SecurityException(String.format("Uid %d has no permission to listen for"
    289                         + " netd events.", uid));
    290             }
    291         }
    292 
    293         @Override
    294         public boolean registerNetdEventCallback(INetdEventCallback callback) {
    295             enforceNetdEventListeningPermission();
    296             if (mNetdListener == null) {
    297                 return false;
    298             }
    299             return mNetdListener.registerNetdEventCallback(callback);
    300         }
    301 
    302         @Override
    303         public boolean unregisterNetdEventCallback() {
    304             enforceNetdEventListeningPermission();
    305             if (mNetdListener == null) {
    306                 // if the service is null, we aren't registered anyway
    307                 return true;
    308             }
    309             return mNetdListener.unregisterNetdEventCallback();
    310         }
    311     };
    312 
    313     private static final ToIntFunction<Context> READ_BUFFER_SIZE = (ctx) -> {
    314         int size = Settings.Global.getInt(ctx.getContentResolver(),
    315                 Settings.Global.CONNECTIVITY_METRICS_BUFFER_SIZE, DEFAULT_BUFFER_SIZE);
    316         if (size <= 0) {
    317             return DEFAULT_BUFFER_SIZE;
    318         }
    319         return Math.min(size, MAXIMUM_BUFFER_SIZE);
    320     };
    321 
    322     private static ArrayMap<Class<?>, TokenBucket> makeRateLimitingBuckets() {
    323         ArrayMap<Class<?>, TokenBucket> map = new ArrayMap<>();
    324         // one token every minute, 50 tokens max: burst of ~50 events every hour.
    325         map.put(ApfProgramEvent.class, new TokenBucket((int)DateUtils.MINUTE_IN_MILLIS, 50));
    326         return map;
    327     }
    328 }
    329