近期接到某客户特殊需求:把Android TV的主板做成显示器的功能,其中有一项需求是:网络端口保留的情况下,关闭网络通路。也就是说用户在网口中插入网线也无法联网。
这个需求的做法大致有这么几种:
1. 修改网络驱动:初始化的时候关闭网络服务,需要 BSP 技术栈相关经验同事处理。
2. 上层关闭网口:
2.1 一种方式是通过串口关闭网口,单次开机过程中,用户都是无法联网的;
2.2 一种方式是通过App接收到开机完成或网络变化的消息时,关闭网口。
其实修改都比较简单,修改驱动的方式比较彻底,但专业性强一点,修改驱动加载逻辑关联的风险及验证比较多,保险起见一般会采用下面方式里面的一种。
现总结一下下面的两种方式:
方式一:串口关闭网口,核心代码如下
#!/system/bin/sh
function_enable_eth0() {
busybox ifconfig eth0 up
}
function_disable_eth0() {
busybox ifconfig eth0 down
}
function_isEth0_enabled() {
result_enabled=$(netcfg | grep "eth0" | grep "UP")
if [[ "${result_enabled}" == "" ]]; then
echo "false"
else
echo "true"
fi
}
function_shutdown_eth0() {
eth0_enabled=$(function_isEth0_enabled)
if [[ "${eth0_enabled}" == "true" ]]; then
function_disable_eth0
fi
}
# 关闭有线网络
function_shutdown_eth0
网络功能的设置需要执行者具备 root 权限,也就是超级用户权限。
将此脚本存放在 /system/bin 下,赋予 root:root 或者 root:shell 用户组,在 init.rc 中植入对应程序的启动即可,时机可以是监听 boot 属性时触发。
方式二:App 调度关闭网口的接口
根据方式一,我们知道关闭有线网口的关闭指令为:busybox ifconfig eth0 down,而应用是可以通过执行 shell 指令来完成功能的调度,所以起始时尝试了此方法,发现并无作用,原因是 App 无执行网络写功能的权限,链路不通。
但根据功能定义来看,既然串口指令可以达到关闭的目的,理论上在App层也有相关的接口可以调度的,这样更符合功能设计和接口设计的初衷。
抱着解决问题的心态,研究下方式一指令会触发哪些过程,于是在串口下,我进行了如下调试:
1. 怀疑 selinux 权限问题:该平台抓 dmesg 日志有问题,于是查看 selinux,发现 selinux 本身就是关闭的,也就是说这个和 selinux 权限没有关系。
2. 看看命令执行的过程中,系统会有哪些日志输出,我按序输入了如下指令:
# 作用是:清除日志缓存区的日志,新的操作会生成新的日志,方便查阅和分析
logcat -b all -c
# 查看最新日志:all,代表所有的日志缓冲区
# -vthreadtime:日志包含线程、时间戳信息
logcat -b all -vthreadtime
# 由于上述指令输入后,发现进程号为 2889 的进程不断有日志输出,所以中断掉上述指令
# 此处筛选出 2889 进程的日志不显示,利于查阅、分析问题
# &:表示将该指令运行在后台
logcat -b all -vthreadtime | grep -v "2889" &
# 关闭有线网口
busybox ifconfig eth0 down
执行后,我们清晰看到如下日志输出:
红色方框标注出来的信息表示:网络信息正在被更新,上方也明显标识着:
EthernetNetworkFactory( 3363): updateInterface: eth0 link down
这里的 3363 代表进程号,查看是 system_server 进程:
做 Android 的看到这个进程,基本都能猜想到:系统服务又在干活了,而这个进程里面的系统服务,都是通过 Binder 体系对外提供的调用。
根据日志信息,我们找到对应平台的代码:EthernetNetworkFactory.java,平台是 Android 5.1 的,这里因公司信息需要保密,此处截图供阅读者参考:
根据打印,可以看到,这个函数被调用时传递的两个参数分别是:eth0 和 false,而此函数是 private 修饰的,只在此类的内部调用,查看其调用的地方在内部类的监听器中:
这个监听器也是私有类,在此类的 start 函数里面便被注册到对应服务中进行网络状态的监听:
在这里,可以看到是被注册到 mNMService 这个变量之中,这个变量是一个基于 Binder 的客户端代理,可以看到其是从 ServiceManager 通过 NETWORKMANAGEMENT_SERVICE 来获取的,其接口声明在与 INetworkManagementService.aidl 之中。于是两个想法在我大脑中闪过:
a. 这个服务在Android应用开发过程中是没见过的,应该是系统对SDK开发人员做的隔离,是隐藏的,不会打包到 SDK 之中,所以也不会有对应的 XXXManager 给客户端使用。
b. 这个网络监听既然是通过这个代理类注册,也就说明底层获取到网络变化的消息时,通过这个代理类将消息逐层往上层传递,那么这个代理类是否有对端口的操作函数。
带着这两个想法,我找到了对应的代码:INetworkManagementService.aidl、接口实现类NetworkManagementService.java。
查看 NetworkManagementService.java 的类声明,可以验证猜想a确实如此;
查看 INetworkManagementService.aidl 的函数声明,可以看到:
这里确实有我们需要的函数:端口列表、端口关、开、配置等各类接口。
于是我们初步的代码出来了:
// INetworkManagementService mNwService;
Object mINetworkManagementServiceProxy;
public void init() {
// IBinder b = ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE);
// mNwService = INetworkManagementService.Stub.asInterface(b);
try {
Class<?> smClass = Class.forName("android.os.ServiceManager");
Method getService = smClass.getMethod("getService", new Class[]{String.class});
getService.setAccessible(true);
IBinder networkManagementBinder = (IBinder) getService.invoke(null, new Object[]{"network_management"});
Class<?> iNMSClass = Class.forName("android.os.INetworkManagementService$Stub");
Method asInterface = iNMSClass.getMethod("asInterface", new Class[]{IBinder.class});
asInterface.setAccessible(true);
mINetworkManagementServiceProxy = asInterface.invoke(null, new Object[]{networkManagementBinder});
Log.d("Debug", "init: mINetworkManagementServiceProxy = " + mINetworkManagementServiceProxy);
} catch (Throwable e) {
Log.d("Debug", "init: get INetworkManagementServiceProxy failed !!! ");
}
}
private void openEth0() {
try {
Method setInterfaceUp = mINetworkManagementServiceProxy.getClass().getMethod("setInterfaceUp", new Class[]{String.class});
setInterfaceUp.setAccessible(true);
setInterfaceUp.invoke(mINetworkManagementServiceProxy, "eth0");
} catch (Throwable e) {
Log.d("Debug", "openEth0: failed !!! ");
}
}
private void closeEth0() {
try {
Method setInterfaceDown = mINetworkManagementServiceProxy.getClass().getMethod("setInterfaceDown", new Class[]{String.class});
setInterfaceDown.setAccessible(true);
setInterfaceDown.invoke(mINetworkManagementServiceProxy, "eth0");
} catch (Throwable e) {
Log.d("Debug", "closeEth0: failed !!! ");
}
}
执行时发现抛出了安全异常,查看源码发现会校验调用者权限声明:
在 AndroidManifest.xml 中添加对应权限:
<uses-permission android:name="android.permission.CONNECTIVITY_INTERNAL" />
此时编辑器提示此权限只有 system app 才可以申请,于是在 AndroidManifest.xml 中声明 systemUID 后重新打包、签名、安装应用,验证OK。
注意:
1. 上述方式适用于:Android 5~9 的平台
2. Android 10 之后的平台权限有所增加,权限检查详见源码:NetworkStack.checkNetworkStackPermission(mContext); 适配时,需要修改 compileSdkVersion 及对应的权限声明,补充如下权限:
<uses-permission android:name="android.permission.NETWORK_STACK" />
<uses-permission android:name="android.permission.MAINLINE_NETWORK_STACK" />
3. 如上权限任意声明一个即可,因为权限校验时,具备任一个权限的授权认为都是有效的,源码传送门PermissionUtils.java在线源码:
public final class PermissionUtils {
/**
* Return true if the context has one of given permission.
*/
public static boolean checkAnyPermissionOf(@NonNull Context context, @NonNull String... permissions) {
for (String permission : permissions) {
if (context.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED) {
return true;
}
}
return false;
}
}