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
17package com.android.server.connectivity;
18
19import android.content.Context;
20import android.net.ConnectivityMetricsEvent;
21import android.net.IIpConnectivityMetrics;
22import android.net.INetdEventCallback;
23import android.net.metrics.ApfProgramEvent;
24import android.net.metrics.IpConnectivityLog;
25import android.os.Binder;
26import android.os.IBinder;
27import android.os.Parcelable;
28import android.os.Process;
29import android.provider.Settings;
30import android.text.TextUtils;
31import android.text.format.DateUtils;
32import android.util.ArrayMap;
33import android.util.Base64;
34import android.util.Log;
35import com.android.internal.annotations.GuardedBy;
36import com.android.internal.annotations.VisibleForTesting;
37import com.android.internal.util.TokenBucket;
38import com.android.server.SystemService;
39import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityEvent;
40import java.io.FileDescriptor;
41import java.io.IOException;
42import java.io.PrintWriter;
43import java.util.ArrayList;
44import java.util.List;
45import java.util.function.ToIntFunction;
46
47/** {@hide} */
48final 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