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