开发板芯片:RK3399
Android版本: 7.1.2

我们在Android开发板上开发一款App,Android开发板有wifi, 4G和以太网接口,App通过以太网与IP摄像头连接获取数据,App处理后通过wifi/4G网络将结果上报到云端。因此,要求Android能同时连接以太网(称为内网)和外网。

熟悉Android的同学可能知道,默认情况下Android在同一时刻只能使用一种网络,比如手机同时连接4G和wifi,会优先使用wifi。因此,实现这个功能需要修改Android ROM,重新烧固件。对于主要从事后端开发的我来讲,看起来挑战挺大的。下面整理了一下解决这个问题的过程和方法。

首先,制定了下面的方案和计划:

  1. 下载开发板的官方编译好的镜像,熟悉固件烧录流程,因为后面会进行多次固件烧录进行测试。
  2. 下载开发板对应的源代码编译镜像,确保编译环境和流程的正确性,因为后面要修改Android的源代码,得首先排除掉编译过程中可能引入的错误。
  3. 重复“修改源代码,编译,烧录固件和测试”这个循环直到问题解决

0. 现状

Android连接上wifi,此时可以正常访问外网。插上网线后,可以访问内网,但是不能访问外网了。查询资料后发现,Android内部对每种类型的网络有个评分,如果几个不同类型的网络都能使用,会使用分数高的网络,查看源代码后发现以太网的默认评分是70(可能不同的版本不一样),高于wifi和4G网络的评分,因此插上网线后就默认使用以太网访问所有IP。很自然的,把以太网的默认评分调为比其他网络类型都低就可以保证wifi/4G不会被踢掉。相关代码在EthernetNetworkFactory.java中,里面有个静态常量定义了以太网的评分。

-    private static final int NETWORK_SCORE = 70;
+    private static final int NETWORK_SCORE = 30; // changed from 70 by mjshi

重新编译后烧录固件,发现插上网线后wifi依然可用,可以访问外网,但是无法访问内网。这符合预期,说明wifi没有因为插上网线后被踢掉。

1. 第一次不太成功的尝试

通过ifconfig可以看到以太网卡和wifi都同时在线,猜测是路由表没有配置好,所有IP的访问请求都默认走wifi了,也许只需要增加一条路由记录,让访问内网192.168.1.0网络的请求走以太网就可以。

按照这个思路,先看看Android的路由表,考虑到Android是基于Linux的,因此直接使用Linux上route命令得到类似如下的路由表:

30.133.236.0/22 dev wlan0  proto kernel  scope link  src 30.133.237.132
192.168.1.0/24 dev eth0  proto kernel  scope link  src 192.168.1.2

咋一看,好像没有问题啊,路由表中相应的路由记录的啊。经过一番查询资料后,发现Android使用了多级路由策略,简单来讲就是Android里面有多张路由表,至于哪张路由表生效是由其优先级决定的(参考这里: )。

通过ip rule list得到类似下面的路由策略(我删除了一些不相干的记录):

0:      from all lookup local
10500:  from all oif wlan0 lookup wlan0
.....
23000:  from all fwmark 0x0/0xffff lookup main
32000:  from all unreachable

可以看到系统中实际其作用的是第二条,最后面的wlan0就是路由表的名称。

通过ip route list table wlan0可以看到其内容,如下:

default via 30.133.239.254 dev wlan0  proto static
30.133.236.0/22 dev wlan0  proto static  scope link

可以看到里面没有到内网的路由记录,因此可以访问外网,但是不能访问内网。那怎么办呢,很自然的,在wlan0中增加一条到内网路由记录,命令如下:

ip route add 192.168.1.0/24 dev eth0 proto static scope link table wlan0

此时,添加了上面的记录后可以访问内网和外网了。不过,这样还没有解决问题,因为这样改了只是临时可以用,如果系统重启或者是通过4G上外网依然无法解决。因此,我们需要在Android系统中某个地方自动把将上面的路由记录添加进去。

很自然想到,可以在wifi和4G网络刚连上的时候,对路由表进行修改,查询了一些资料后在ConnectivityService.java中的对应地方加了一些代码,尝试后不成功,主要原因是这一层是通过Android封装的接口对路由表进行修改,有些限制。而且Android代码很大(解压缩以后有25GB),不可能导入到IDE里面进行修改,修改代码效率很低。放弃这条路子,继续查询资料。

2. 第二次尝试

查询了资料了解到,Android可以在init.rc中定义service,然后设置指定的系统property就可以触发service对应的脚本。
于是,我先写了脚本来增加一条优先级较高的路由表:

#!/system/bin/sh
if [ $# -ne 1 ];then
   echo "usage: $0 [add|del]"
   exit 1
fi

action=$1
if [ "$action" == "add" ];then
  # avoid add the rule twice
  ip rule list | grep '^9000:.*1'
  if [ $? -ne 0 ];then
    ip rule add from all table 1 pref 9000
  fi
  ifconfig eth0 192.168.1.2 netmask 255.255.255.0 up
  ip route add 192.168.1.0/24 via 192.168.1.2 dev eth0 table 1
  setprop eth.route ""
elif [ "$action" == "del" ];then
  ip rule del pref 9000
  setprop eth.route ""
else
  echo "unsupported action: $action"
fi

这个脚本很简单,就是增加一个优先级为9000的路由表名称为1(好像只能命名为数字),里面定义了到内网的路由记录。这样的好处是,我们不用修改默认的路由表(wlan0如果连的是wifi,wwan0如果连的是4G)。

然后在init.rc中定义了两个services:

service add_eth_route /system/bin/eth_route.sh add
    class main
    disabled
    oneshot

service del_eth_route /system/bin/eth_route.sh del
    class main
    disabled
    oneshot

on property:eth.route=add
    start add_eth_route

on property:eth.route=del
    start del_eth_route

它的意思是说,如果系统属性eth.route的值为add,则执行命令/system/bin/eth_route.sh add。如果为del的话,则执行/system/bin/eth_route.sh del

最后,在java代码中找到以太网卡插上网线和拔掉网线的地方,设置系统属性eth.route为对应的值,代码还是在EthernetNetworkFactory中。至于是怎么找到应该改在这里,我是在logcat的日志中发现eth0 link up

+        String ethRoutePropKey = "eth.route";
+        if (up) {
+            Log.d(TAG, "updateInterface: " + iface + " link up, setup route...");
+            SystemProperties.set(ethRoutePropKey, "add");
+        } else {
+            Log.d(TAG, "updateInterface: " + iface + " link down, removing route...");
+            SystemProperties.set(ethRoutePropKey, "del");
+        }

采用这个方案的好处是java中代码改动很少,这样出错的几率就小。

编译、打包、烧录固件,期待能一次成功,结果是这个脚本没有被调用起来,又排查了很久在dmesg中找到了下面的错误日志:

Service xxx does not have a SELinux domain defined

有错误日志就好办,很快找到了解决办法: 按照里面的方法修改后终于可以同时访问内网和外网了。

3. 测试

测试了以下场景

  • 插上网线冷启动
  • 拔掉网线不能上内网,但外网访问不受影响。插上网线后能上内网,外网也正常。
  • 禁用wifi/4G,内网访问不受影响。再次开启wifi/4G,恢复正常
    其中,在第三个case中,禁用wifi/4G,内网也不能访问了,原因是此时只有以太网可用,Android系统将以太网提升为默认网络,会修改路由表,导致路由表错乱(具体原因没有去查了),实际上此时以太网已经处于连接状态且配置正确,没有必要再去进行配置了。因此,改动很简单,在对应的地方增加下面代码:
+        if (mEthernetCurrentState != EthernetManager.ETHER_STATE_DISCONNECTED) {
+            Log.d(TAG, "onRequestNetwork: " + mIface + "current state = " + mEthernetCurrentState);
+            return;
+        }

编译,烧录固件,测试一切符合预期,可以把chrome中几十个tab一下全关掉了,真爽!

4. 总结

解决问题总是有一些共有的套路的:

  1. 工具链一定先弄好,避免犯一些低级的错误,比如代码改动没有生效
  2. 多看日志,一开始可能日志很多不知道从哪里开始看,那就从后面往前看
  3. 搜索,中英文关键字都可以用,这次很多资料就是在中文资料上找到的。
  4. 每次改动的内容要少一点,否则一下子改很多东西,也不知道是哪个改动在发生作用。

最后感谢很多朋友分享的文章,没有你们的经验分享,我也无法解决这个问题,因此整理成文希望能帮到更多的朋友。