Android 流量监控方案设计

1,971 阅读21分钟

需求

1 客户端功能概述

  • 自动监控各APP流量消耗 通过健康监控APP,自动采集DHU系统内各APP的流量信息,包括流量消耗量、日期、前台 or 后台、上行流量 or 下行流量、移动流量 or WLAN流量。将采集到的信息本地缓存后,周期性上传到服务端,服务端对流量数据进行大数据分析。
  • 流量异常事件 服务端对上传的流量信息进行大数据分析后,按照自定义规则判定流量异常事件,例如对每个应用设置流量阈值,当流量超过阈值时判定为流量异常事件。服务端将异常事件下发给客户端,客户端收到异常事件消息后,抓取对应logcat日志并回传到服务端。
  • 同步服务端监控策略 客户端周期性(待定)拉取服务端配置的监控策略和异常事件信息,并更新本地缓存的策略以及处理流程异常事件。

2 后台服务端 功能概述

  • 远程配置监控参数 服务端可配置流量监控参数,包括功能开关(VIN码)、流量信息上传周期、流量异常事件等。

  • 流量异常事件 服务端对上传的流量信息进行大数据分析,按照自定义规则判定流量异常事件,例如对每个应用设置流量阈值,当流量超过阈值时判定为流量异常事件,并将异常事件下发给客户端,客户端抓取异常事件日志并回传。

  • 流量数据分析 服务端对上传的流量信息进行大数据分析,生成各类报表。例如APP流量排行、车机流量排行、单机流量详情、流量异常事件管理等。

技术调研

调研到两种方式获取流量信息。

1. 读取系统文件

  • /proc/net/xt_qtaguid/stats:中包含所有PID(应用)的wlan(WiFi)和rmnet(2G/3G)、lo(本地)流量信息。并区分前台和后台流量。
  • /proc/net/dev:统计系统总的流量信息
  • /proc/uid_stat/uid/tcp_rcv:单个应用的流量统计信息

读取文件方案适用于Android原生,在车机系统中无相应文件,需继续调研车机系统中是否有类似文件信息。

验证结果

车机系统中无/proc/net/xt_qtaguid/stats文件。

通过Android源码查看,Android系统获取流量信息的方式最终走入native方法,使用的是Linux内核共享内存的方式,无法通过应用层查询到。只能使用方案2,通过AndroidAPI查询。


      /**
       * Reads the detailed UID stats based on the provided parameters
       *
       * @param limitUid the UID to limit this query to
       * @param limitIfaces the interfaces to limit this query to. Use {@link312       *     NetworkStats.INTERFACES_ALL} to select all interfaces
       * @param limitTag the tags to limit this query to
       * @return the NetworkStats instance containing network statistics at the present time.
       */
      public NetworkStats readNetworkStatsDetail(
              int limitUid, String[] limitIfaces, int limitTag) throws IOException {
18          // In order to prevent deadlocks, anything protected by this lock MUST NOT call out to other319          // code that will acquire other locks within the system server. See b/134244752.320          synchronized (mPersistentDataLock) {
              // Take a reference. If this gets swapped out, we still have the old reference.322              final VpnInfo[] vpnArray = mVpnInfos;
              // Take a defensive copy. mPersistSnapshot is mutated in some cases below324              final NetworkStats prev = mPersistSnapshot.clone();
  
              if (USE_NATIVE_PARSING) {
                  final NetworkStats stats =
                          new NetworkStats(SystemClock.elapsedRealtime(), 0 /* initialSize */);
                  if (mUseBpfStats) {
                      try {
                          requestSwapActiveStatsMapLocked();
                      } catch (RemoteException e) {
                          throw new IOException(e);
                      }
                      // Stats are always read from the inactive map, so they must be read after the336                      // swap337                      if (nativeReadNetworkStatsDetail(stats, mStatsXtUid.getAbsolutePath(), UID_ALL,
                              INTERFACES_ALL, TAG_ALL, mUseBpfStats) != 0) {
                          throw new IOException("Failed to parse network stats");
                      }
  
                      // BPF stats are incremental; fold into mPersistSnapshot.343                      mPersistSnapshot.setElapsedRealtime(stats.getElapsedRealtime());
                      mPersistSnapshot.combineAllValues(stats);
                  } else {
                      if (nativeReadNetworkStatsDetail(stats, mStatsXtUid.getAbsolutePath(), UID_ALL,
                              INTERFACES_ALL, TAG_ALL, mUseBpfStats) != 0) {
                          throw new IOException("Failed to parse network stats");
                      }
                      if (SANITY_CHECK_NATIVE) {
                          final NetworkStats javaStats = javaReadNetworkStatsDetail(mStatsXtUid,
                                  UID_ALL, INTERFACES_ALL, TAG_ALL);
                          assertEquals(javaStats, stats);
                      }
  
                      mPersistSnapshot = stats;
                  }
              } else {
                  mPersistSnapshot = javaReadNetworkStatsDetail(mStatsXtUid, UID_ALL, INTERFACES_ALL,
                          TAG_ALL);
              }
  
              NetworkStats adjustedStats = adjustForTunAnd464Xlat(mPersistSnapshot, prev, vpnArray);
  
              // Filter return values366              adjustedStats.filter(limitUid, limitIfaces, limitTag);
              return adjustedStats;
          }
      }

2. 使用Android原生API

Android原生API有TrafficStats(已过时)和NetworkStatsManager,NetworkStatsManager NetworkStatsManager - Android中文版 - API参考文档

提供的API可区分WIFI和MOBILE流量,可区分各UID(应用分配ID)流量,上行和下行流量。但不能满足的需求有以下两点。

  • 不能区分前台流量和后台流量

    • 解决办法: 通过周期查询前台应用 appProcess.importance == RunningAppProcessInfo.IMPORTANCE_BACKGROUND 判断周期内流量为前台还是后台,
    • 缺陷: 周期内的应用前后台变化会导致前后台流量区分不够准确。
  • 同UID的应用无法拆开细分(车机内大量系统应用使用shareUID方案,使用此方案的应用UID都为系统UID:1000)。使用系统版本 20.32.20.000001.230322_userdebug 统计,总共具有 android.permission.INTERNET 权限的应用79个,其中使用ShareUID 38个,非ShareUID 41个,意味着系统中将有38个应用无法被流量监控方案覆盖。

    • 解决办法: 通过双向认证SDK 统计各应用上行和下行的https流量包大小,然后跨进程发送给健康监控收集数据@贺康 补充双向认证中流量监控方案。
    • 缺陷: 双向认证增加跨进程通信和流量统计会变重,统计到的数据只能是https接入双向认证的流量。且HTTP,FTP、UDP等流量无法统计到。目前双向认证的aar包只有我们内部app在使用。

3. 开发aar包,各应用集成,应用层采集上行和下行流量

待定,各业务方需集成多余的包。

4. 通过VPN过滤的方式统计流量

待定,需framework层提供协助,具体总入口为/frameworks/base/services/core/java/com/android/server/ConnectivityService.java 的**linkPropertiesRestrictedForCallerPermissions** 方法。

1724      private LinkProperties linkPropertiesRestrictedForCallerPermissions(
1725              LinkProperties lp, int callerPid, int callerUid) {
1726          if (lp == null) return new LinkProperties();
1727  
1728          // Only do a permission check if sanitization is needed, to avoid unnecessary binder calls.1729          final boolean needsSanitization =
1730                  (lp.getCaptivePortalApiUrl() != null || lp.getCaptivePortalData() != null);
1731          if (!needsSanitization) {
1732              return new LinkProperties(lp);
1733          }
1734  
1735          if (checkSettingsPermission(callerPid, callerUid)) {
1736              return new LinkProperties(lp, true /* parcelSensitiveFields */);
1737          }
1738  
1739          final LinkProperties newLp = new LinkProperties(lp);
1740          // Sensitive fields would not be parceled anyway, but sanitize for consistency before the1741          // object gets parceled.1742          newLp.setCaptivePortalApiUrl(null);
1743          newLp.setCaptivePortalData(null);
1744          return newLp;
1745      }

此方法为网络请求进入的framework层总入口,可以根据pid判断对应的包的连接,根据持有的LinkProperties对象能否拿到上下行bytes统计,需要和framework层再深入对接。

www.aospxref.com/android-11.…

设计方案

1. 流程图

加粗部分为周期性任务,需要特别关注性能和优化。

标黄部分为设想优化项。

2. 相关问题和待优化项

  • 健康监控流量采集中的各周期和流量变化阈值的设定,还需要写个demo装入实车,收集一些具体使用情况后设计。且需要采用可配置的参数。
  • 流量采集误差优化,建议后台根据实际上传总流量和统计到的流量信息,做算法优化。
  • 和framework层沟通,能否监听前后台应用切换,不采用轮询方式优化
  • wifi状态下,App也可能走的是流量,这个是由Google原生的网络评级决定的,可能产生部分误差

补充

1. 读取系统netstat文件和proc/net/tcp等文件方式获取

netstat -anep

net_anep.txt:

Active Internet connections (established and servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       User       Inode       PID/Program Name
tcp        0      0 127.0.0.1:5354          0.0.0.0:*               LISTEN      1000       71150       3263/com.ts.carplay
tcp        0      0 0.0.0.0:7000            0.0.0.0:*               LISTEN      1000       73130       3263/com.ts.carplay
tcp        0      0 127.0.0.1:12345         0.0.0.0:*               LISTEN      0          85404       6398/admfotaapp
tcp        0      0 0.0.0.0:5000            0.0.0.0:*               LISTEN      1000       73132       3263/com.ts.carplay
tcp        0      0 198.18.34.15:50351      198.18.34.2:40500       ESTABLISHED 0          27444       624/vsomeipd
tcp        0      0 127.0.0.1:12345         127.0.0.1:43932         ESTABLISHED 0          102886      6398/admfotaapp
tcp        0      0 198.18.34.15:50366      198.18.34.2:30580       ESTABLISHED 0          53345       624/vsomeipd
tcp        0      0 198.18.34.15:50340      198.18.34.2:30562       ESTABLISHED 0          27474       624/vsomeipd
tcp       70      0 127.0.0.1:53486         127.0.0.1:5354          ESTABLISHED 1000       68590       3263/com.ts.carplay
tcp        0      0 198.18.34.15:50380      198.18.34.2:30521       ESTABLISHED 0          54320       624/vsomeipd
tcp        0      0 127.0.0.1:5354          127.0.0.1:53486         ESTABLISHED 1000       73240       3263/com.ts.carplay
tcp        0      0 198.18.34.15:50346      198.18.34.2:30556       ESTABLISHED 0          27465       624/vsomeipd
tcp        0      0 198.18.34.15:50359      198.18.34.2:30559       ESTABLISHED 0          54350       624/vsomeipd
tcp        0      0 198.18.34.15:50343      198.18.34.2:30579       ESTABLISHED 0          27470       624/vsomeipd
tcp        0      0 198.18.34.15:50666      198.18.34.2:33332       ESTABLISHED 0          54308       624/vsomeipd
tcp        0      0 198.18.34.15:50364      198.18.34.2:30563       ESTABLISHED 0          27439       624/vsomeipd
tcp        0      0 127.0.0.1:53490         127.0.0.1:5354          ESTABLISHED 1000       73244       3263/com.ts.carplay
tcp        0      0 10.167.45.72:33670      36.150.102.196:7824     ESTABLISHED 10042      1332388     13498/com.bilibili.bilithings
tcp        0      0 127.0.0.1:43932         127.0.0.1:12345         ESTABLISHED 0          102882      6371/admsysi4deployapp
tcp   1179448      0 10.167.45.72:52164      114.97.122.72:8000      ESTABLISHED 10042      1438217     13822/com.bilibili.bilithings:ijkservice
tcp        0      0 198.18.34.15:50349      198.18.34.128:40000     ESTABLISHED 0          52069       624/vsomeipd
tcp        0      0 127.0.0.1:12345         127.0.0.1:43918         ESTABLISHED 0          85725       6398/admfotaapp
tcp        0      0 198.18.34.15:50352      198.18.34.2:30999       ESTABLISHED 0          53350       624/vsomeipd
tcp        0      0 198.18.34.15:50341      198.18.34.2:30553       ESTABLISHED 0          46398       624/vsomeipd
tcp    58624      0 10.167.45.72:55636      123.15.88.116:8000      ESTABLISHED 10042      1424024     13822/com.bilibili.bilithings:ijkservice
tcp        0      0 198.18.34.15:43822      198.18.34.2:30558       ESTABLISHED 0          54364       624/vsomeipd
tcp        0      0 198.18.34.15:50348      198.18.34.128:40002     ESTABLISHED 0          52065       624/vsomeipd
tcp        0      0 198.18.34.15:50381      198.18.34.128:40001     ESTABLISHED 0          52061       624/vsomeipd
tcp        0      0 198.18.34.15:45102      198.18.34.2:30538       ESTABLISHED 0          27450       624/vsomeipd
tcp        0      0 198.18.34.15:50369      198.18.34.2:30565       ESTABLISHED 0          50170       624/vsomeipd
tcp        0      0 198.18.34.15:50370      198.18.34.2:30561       ESTABLISHED 0          46372       624/vsomeipd
tcp        0    272 198.18.34.15:53548      198.18.34.2:30590       ESTABLISHED 0          41627       624/vsomeipd
tcp        0      0 127.0.0.1:5354          127.0.0.1:53490         ESTABLISHED 1000       73249       3263/com.ts.carplay

tcp.txt:

sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode                                                     
   0: 0100007F:14EA 00000000:0000 0A 00000000:00000000 00:00000000 00000000  1000        0 71150 1 0000000000000000 100 0 0 10 0                     
   1: 00000000:1B58 00000000:0000 0A 00000000:00000000 00:00000000 00000000  1000        0 73130 1 0000000000000000 100 0 0 10 0                     
   2: 0100007F:3039 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 85404 1 0000000000000000 100 0 0 10 0                     
   3: 00000000:1388 00000000:0000 0A 00000000:00000000 00:00000000 00000000  1000        0 73132 1 0000000000000000 100 0 0 10 0                     
   4: 0F2212C6:C4AF 022212C6:9E34 01 00000000:00000000 02:0000015B 00000000     0        0 27444 2 0000000000000000 20 4 0 10 -1                     
   5: 0100007F:3039 0100007F:AB9C 01 00000000:00000000 00:00000000 00000000     0        0 102886 1 0000000000000000 20 4 30 10 -1                   
   6: 0F2212C6:C4BE 022212C6:7774 01 00000000:00000000 02:0000010E 00000000     0        0 53345 2 0000000000000000 20 0 0 10 -1                     
   7: 482DA70A:C676 A173FC3A:1F40 01 00000000:0000E660 00:00000000 00000000 10042        0 1477214 1 0000000000000000 24 11 0 10 -1                  
   8: 0F2212C6:C4A4 022212C6:7762 01 00000000:00000000 02:00000041 00000000     0        0 27474 2 0000000000000000 51 0 0 10 -1                     
   9: 0100007F:D0EE 0100007F:14EA 01 00000000:00000046 00:00000000 00000000  1000        0 68590 1 0000000000000000 20 5 28 10 -1                    
  10: 0F2212C6:C4CC 022212C6:7739 01 00000000:00000000 02:0000015B 00000000     0        0 54320 2 0000000000000000 20 4 0 10 -1                     
  11: 0100007F:14EA 0100007F:D0EE 01 00000000:00000000 00:00000000 00000000  1000        0 73240 1 0000000000000000 20 4 28 10 -1                    
  12: 0F2212C6:C4AA 022212C6:775C 01 00000000:00000000 02:0000015B 00000000     0        0 27465 2 0000000000000000 20 4 0 10 -1                     
  13: 0F2212C6:C4B7 022212C6:775F 01 00000000:00000000 02:0000008E 00000000     0        0 54350 2 0000000000000000 22 0 0 10 -1                     
  14: 0F2212C6:C4A7 022212C6:7773 01 00000000:00000000 02:0000010E 00000000     0        0 27470 2 0000000000000000 20 0 0 10 -1                     
  15: 0F2212C6:C5EA 022212C6:8234 01 00000000:00000000 02:0000010E 00000000     0        0 54308 2 0000000000000000 20 0 0 10 -1                     
  16: 0100007F:AB8E 0100007F:3039 01 00000000:00000000 00:00000000 00000000  1000        0 85721 1 0000000000000000 20 4 30 10 -1                    
  17: 0F2212C6:C4BC 022212C6:7763 01 00000000:00000000 02:0000010E 00000000     0        0 27439 2 0000000000000000 20 0 0 10 -1                     
  18: 0100007F:D0F2 0100007F:14EA 01 00000000:00000000 00:00000000 00000000  1000        0 73244 1 0000000000000000 20 4 28 10 -1                    
  19: 482DA70A:8386 C4669624:1E90 01 00000000:00000000 00:00000000 00000000 10042        0 1332388 1 0000000000000000 23 4 2 10 -1                   
  20: 0100007F:AB9C 0100007F:3039 01 00000000:00000000 00:00000000 00000000     0        0 102882 1 0000000000000000 20 4 30 10 -1                   
  21: 0F2212C6:C4AD 802212C6:9C40 01 00000000:00000000 02:00000027 00000000     0        0 52069 2 0000000000000000 20 4 0 10 -1                     
  22: 0100007F:3039 0100007F:AB8E 01 00000000:00000000 00:00000000 00000000     0        0 85725 1 0000000000000000 20 4 30 10 -1                    
  23: 0F2212C6:C4B0 022212C6:7917 01 00000000:00000000 02:0000010E 00000000     0        0 53350 2 0000000000000000 20 0 0 10 -1                     
  24: 0F2212C6:C4A5 022212C6:7759 01 00000000:00000000 02:0000015A 00000000     0        0 46398 2 0000000000000000 20 4 0 10 -1                     
  25: 482DA70A:BC80 FC058524:1180 01 00000000:0010297C 00:00000000 00000000 10042        0 1453653 1 0000000000000000 26 4 0 10 -1                   
  26: 0F2212C6:AB2E 022212C6:775E 01 00000000:00000000 02:0000015A 00000000     0        0 54364 2 0000000000000000 20 4 0 10 -1                     
  27: 0F2212C6:C4AC 802212C6:9C42 01 00000000:00000000 02:00000041 00000000     0        0 52065 2 0000000000000000 20 4 0 10 -1                     
  28: 0F2212C6:C4CD 802212C6:9C41 01 00000000:00000000 02:000001C1 00000000     0        0 52061 2 0000000000000000 20 4 1 2 7                       
  29: 0F2212C6:B02E 022212C6:774A 01 00000000:00000000 02:000001C1 00000000     0        0 27450 2 0000000000000000 20 4 0 10 -1                     
  30: 0F2212C6:C4C1 022212C6:7765 01 00000000:00000000 02:0000010E 00000000     0        0 50170 2 0000000000000000 20 0 0 10 -1                     
  31: 0F2212C6:C4C2 022212C6:7761 01 00000000:00000000 02:0000010E 00000000     0        0 46372 2 0000000000000000 20 0 0 10 -1                     
  32: 0F2212C6:D12C 022212C6:777E 01 00000000:00000000 02:00028E76 00000000     0        0 41627 2 0000000000000000 24 0 0 18 18                     
  33: 0100007F:14EA 0100007F:D0F2 01 00000000:00000000 00:00000000 00000000  1000        0 73249 1 0000000000000000 20 4 26 10 -1                    

netstat中的inode对应tcp文件中的inode,能对应到具体应用(包名和pid),但两个文件的tx和rx数据都不能统计具体流量上行和下行的大小。

2. 修改framework代码

1. Android网络调用时序(TCP)

时序图如下:

2. Android网络调用流程

  1. TCP包含http和https,https比http仅多一步安全证书校验。
  2. 应用层调用一般采用三种方式,HttpURLConnection(Android原生),HttpClient(Android老版),OKHttp。
  3. 应用层的http和https、FTP调用最终都进行到RealConnection,后调用Socket进行connect然后通信。
  4. UDP也是使用Socket进行通信(待验证)
  5. Socket中使用BufferSink(OutputStream),BufferSource(InputStream)进行流的操作(Android11)

3. 流量监控前置条件

  1. RealConnection、BufferSink、BufferSource都属于aosp-caf/external/okhttp/包下,打包后都在/apex/com.android.art/javalib/okhttp.jar中

  2. Android应用使用到okhttp.jar时,使用的是Android系统javalib中找到的第一个同名类

  3. 且这些类加载和运行在app的进程中

   /** Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */
    private void connectSocket(int connectTimeout, int readTimeout, int writeTimeout,
        ConnectionSpecSelector connectionSpecSelector) throws IOException {
      rawSocket.setSoTimeout(readTimeout);
      try {
        Platform.get().connectSocket(rawSocket, route.getSocketAddress(), connectTimeout);
      } catch (ConnectException e) {
        throw new ConnectException("Failed to connect to " + route.getSocketAddress());
      }
      source = Okio.buffer(Okio.source(rawSocket));
      sink = Okio.buffer(Okio.sink(rawSocket));
  
      if (route.getAddress().getSslSocketFactory() != null) {
        connectTls(readTimeout, writeTimeout, connectionSpecSelector);
      } else {
        protocol = Protocol.HTTP_1_1;
        socket = rawSocket;
      }
  
      if (protocol == Protocol.SPDY_3 || protocol == Protocol.HTTP_2) {
        socket.setSoTimeout(0); // Framed connection timeouts are set per-stream.
  
        FramedConnection framedConnection = new FramedConnection.Builder(true)
            .socket(socket, route.getAddress().url().host(), source, sink)
            .protocol(protocol)
            .build();
        framedConnection.sendConnectionPreface();
  
        // Only assign the framed connection once the preface has been sent successfully.
        this.framedConnection = framedConnection;
      }
    }

4. 通过hook Socket方式流量监控方案设计

  1. 新建两个类BufferSinkStatistics(extents BufferSink),BufferSourceStatistics(extents BufferSource),重写Buffer的in和out方法,添加统计逻辑。这两个类负责统计Socket的上行流量和下行流量。
  2. 新建两个类中使用反射获取App的包名,将统计数据携带包名跨进程传入server端。
  3. 替换RealConnection(java.net.Socket)中的BufferSink和BufferSource为重写的两个类,使所有上下行的网络请求流都会进行数据统计。
  4. 替换后的编译/apex/com.android.art/javalib/okhttp.jar,所有在此系统中运行的应用,使用到http、https、(UDP)的请求,都会纳入统计。

此方案为理论设计,尚待验证。

缺陷:

  1. 需要频繁进行流量计算,每一个socket都需要计算,计算后还需要进行跨进程通信进行统计。
  2. 是否会统计到内网上下行流量,需要验证。

参考资料

  1. Android UID的分配、查看及相关知识:blog.csdn.net/lee17112217…
  2. 两种流量监控方案:blog.csdn.net/dexFeng/art…
  3. /proc/net/xt_qtaguid/stats文件监测流量信息:www.cnblogs.com/weilf/p/918…
  4. TrafficStats方案获取流量信息:blog.csdn.net/weixin_3418…
  5. 车机网络状态监控实测: blog.csdn.net/android_cai…
  6. Android网络评分机制:blog.csdn.net/weixin_5001…