前言
前几篇介绍了iptables | 路由策略 | DNS等相关理论基础知识,现在在这基础上,去学习安卓的网络框架并应用这些知识点。Android的网络框架可以细分很多部分,按功能分的话,可以分网络评分与选择,apn管理,网络策略管理等;按层次分的话,可以分framework部分,native netd部分。现在需要定制一些iptables规则,然后应用到安卓源码。主要目的在于:
- iptables规则是在android源码的哪个地方添加?
- 如何给上层提供网络相关的接口,涉及到framework/native哪部分代码?
带着上面的两个目的,本文主要介绍netd和NetworkManagementService。弄懂这两部分的流程,就可以定制iptables规则,向上提供接口,管理网络相关的功能。
基本需求
要学习安卓网络框架相关的知识,先确定你想通过它来达到什么目的。目前的想法就是做个demo app,实现下面的功能。
- 显示当前网络信息,网口名称/IP地址/DNS地址/连接状态等。
- 建立白名单模式和黑名单模式,允许/屏蔽某个IP地址访问网络。
- 查询网址的IP地址,方便我们屏蔽或放行。
- 提供屏蔽/允许DNS查询功能。
带着上面等待实现的小功能,开始学习安卓网络框架。
netd(Network Daemon)
基本概念
Network Daemon是Android系统中专门负责网络管理和控制的后台守护进程。它封装了底层的各种网络,隔离了底层网络接口的差异,给Framework提供了统一调用接口,简化了网络的使用。它通过netlink,虚拟文件系统等linux内核提供的用户接口,与内核通信,管理网络相关部分。主要功能有三部分:
- 设置防火墙(Firewall),网络地址转换(NAT),带宽控制,流量统计,无线网上软接入点(soft acess point)控制,网络设备绑定(Tether),配置路由表,interface管理等。
- Android系统中的DNS信息缓存和管理。
- 网络服务搜索(Net Service Discovery, NSD)功能。
netd位于framework层和kernel层中间,是Android系统中网络相关消息和命令转发及处理的中枢模块。netd的工作流程可分为两部分:
- netd接收并处理来自framework层的NetworkManagementService或NsdService的命令。
- netd接收并解析来自kernel的UEvent消息,然后再转发给framework层对应的service去处理。
启动与初始化
手机启动的过程会加载netd.rc文件,启动netd进程。文件如下所示:
android/system/netd/server/netd.rc
从这里看出,配置了几个socket(dnsproxyd, mdns, fwmarkd),netd进程初始化时会去监听这几个socket,上层可以通过这些socket和netd进行通信。
SOCKET | 描述 |
---|---|
dnsproxyd | DNS代理的控制与配置 |
mdns | 多播DNS,Multicast DNS |
fwmarkd | iptables的策略路由配置,流量打标签,设置网络权限等 |
接着看一下netd的main函数,代码删去部分细节,只留主逻辑,下图标出来的就是重点要学习的地方。
从这里看出,netd的main函数很简洁,主要是对rc文件中三个socket进行监听,并初始化NetlinkManager,NetdNativeService,Controller。接下来,对上面标出的几个类做详细的分析。
NetlinkManager
NetlinkManager主要负责接收并解析来自kernel的UEvent消息,这里不贴代码。它的初始化流程图如下所示:
从上面看出,它创建了四个不同协议的socket和对应的handler对象,每个socket都会对应的创建一个线程进行poll,监听不同协议的kernel事件。当有事件来,便会调用SocketListener的onDataAvailable方法,该方法内部会创建一个NetlinkEvent对象。NetlinkEvent对象根据socket创建时指定的解析类型去解析来自kernel的消息,最后onEvent方法被调用,不同类型的事件在这个方法中进行分类处理,它调用EventReporter通过binder的方式向framework层NetworkManagementService发起回调,调用对应的notifyXXX方法。下面是四个socket的描述:
协议 | 描述 |
---|---|
NETLINK_KOBJECT_UEVENT | 反映网络设备的事件和状态,如网口状态的变化 |
NETLINK_ROUTE | 接收路由信息, 如路由表的更新与删除 |
NETLINK_NFLOG | 接收数据流量使用配额的消息, 如数据使用超限 |
NETLINK_NETFILTER | 接收包过滤(netfilter)的消息 |
再来看一下NetlinkManager涉及到的几个类的类图,NetlinkManager->NetlinkHanler->NetlinkListener->SocketListener.
从这个类图看,NetlinkManager启动NetlinkHandler之后,就交给它去处理事件了。NetlinkHandler继承NetlinkListener, NetlinkListener继承SocketListener。SocketListener主要是处理socket相关的逻辑,起线程监听kernel事件。NetlinkListener主要是创建NetlinkEvent,把读取到的事件消息放在这里进行解析。最后NetlinkHandler负责把解析后的消息,传递给上层framework。可以看到每个类都有自己的业务逻辑,清晰明了。
Controllers
Controllers顾名思义,是一系列的控制器,包括防火墙,流量统计,带宽,路由表等功能的控制器;它的初始化流程如下图所示。
可以看到,Controllers的主要作用就是创建了很多规则子链,用来控制网络相关的功能。比较老的android版本,都是通过iptables创建规则链来实现这些功能的;但是android引入了bpf之后,很多都是通过bpf来实现,包括数据包的过滤和拦截,app的上网控制等。像之前的fw_dozable,fw_standby,fw_powersave规则子链,都没有创建了,改成采用bpf过滤的方式。而bpf的相关功能实现,是通过TrafficController关联bpf来实现的。
Controllers的成员变量,包括了下面的各个controller:
控制器 | 描述 |
---|---|
NetworkController | 保存网络信息,包括NetId | 接口 | IP地址 |
TetherController | 网络设备绑定,如通过USB共享网络 |
PppController | tty终端绑定pppd(点对点协议的守护进程,在使用VPN时才需要使用它) |
BandwidthController | 带宽控制,实际是监控系统数据流量,流量阈值提醒,流量使用上限,限制app后台流量 |
IdletimerController | 监听网口在设定时间内没有数据通过时,上报一个netlink事件 |
FirewallController | 防火墙功能,制定防火墙规则 |
ClatdController | IPv4-IPv6 地址的转换器(不确定) |
StrictController | 检测app运行网络通信是否进行SSL/TLS数据加密 |
EventReporter | 负责事件上报到framework,提供注册接口,保存Listener |
IptablesRestoreController | 负责执行iptables命令 |
WakeupController | 不懂 |
XfrmController | 与Ipsec相关 |
TrafficController | 与eBPF结合,负责对数据包过滤,流量统计,按UID划分防火墙等 |
TcpSocketMonitor | 定时监听物理网络tcp相关信息,包括发包/丢包数目,rtt等等 |
通过上面这个表格,对这些controller我们有了初步的了解,知道它大致的功能,日后需要调查具体的细节,在回过头去查找就可以。也看到了iptables规则的创建,就在这些controller之中。由此推测,假如需要定制一些网络功能,那么可能要在这些controller中增加iptables规则。
FwmarkServer
FwmarkServer的作用就是给数据打标签。从前面的netd.rc文件中已经配置socket_name为fwmarkd,netd进程会去初始化,进行监听。请看下面流程图:
从这里看出,主要的逻辑是在processClient中进行处理。其中的逻辑包括,在socket进行connect时,accept完成时,根据具体条件对socket的fwmark进行设置(fwmark包括permission,netid两部分)。这其实是对socket相关系统函数(connect|accept)进行hook,在hook函数中通过链接到FwmarkServer进行打标签。Hook流程和数据进来打标签可以参考:Android 策略路由
NetdNativeService
NetdNativeService是一个binder服务,它是framework层和HAL层通信的媒介。它发布一个netd服务供framework层使用,实现INetd.aidl接口。
system\netd\server\binder\android\net\INetd.aidl
NetdNativeService的功能实际上是通过调用Controller去实现的,所以说它只是通信的媒介,具体功能实现是放在Controller。那么想要增加新的接口,似乎可以在这里增加。
下面是它的初始化流程:
NetworkManagementService
NetworkManagementService在SystemServer进程中启动,它主要用于获取netd服务和native层通信,对网络进行管理。注册监听事件(unsolicited event listener)到netd,用于native上报网络事件。同时获取IOemNetd实例,实际它是一系列接口,如果要新增接口,可以放在这个IOemNetd对象里面。而NetworkManagementService本身也是一个服务,SystemServer把它发布到系统中,供其他模块使用,服务名是"network_management"。
下面是它的初始化流程
从上面看出,启动后时主要做了几件事
- 获取netd服务,通过它和native层通信
- 注册listener到native层监听网络事件
- 获取IOemNetd对象,是一系列接口,扩展用
- 发布自身服务到系统
到这里,初步可以回答本文开头的问题了。
-
iptables相关的规则,需要在native netd中添加,可以参考controller中的实现。
-
增加接口,需要在framework的NetworkManagementService添加;也需要在native层的INetd.aidl/IOemNetd.aidl中添加,在native为了方便区分系统自带的接口和自定义接口,统一放在IOemNetd.aidl这里添加便可以。上层通过获取netd服务,调用接口
IOemNetd.Stub.asInterface(mNetdService.getOemNetd());
得到IOemNetd的实例OemNetdListener,再调用具体接口。实现IOemNetd.aidl的具体对象如下:
system\netd\server\OemNetdListener.cpp
方案实现
这里可以简单分三步,按照下面的步骤去实现功能就行。
- 设计接口(netd & framework)
- 实现接口(iptables规则)
- 编写APK(UI界面,编译源码)
netd 和 framework 涉及修改的文件如下:
system\netd\server\binder\com\android\internal\net\IOemNetd.aidl
system\netd\server\OemNetdListener.h
system\netd\server\OemNetdListener.cpp
frameworks\base\core\java\android\os\INetworkManagementService.aidl
frameworks\base\services\core\java\com\android\server\NetworkManagementService.java
其中接口实现是放在OemNetdListener.cpp。framework增加相应的接口给APK调用。
设计接口
- 黑名单模式屏蔽IP地址
- 白名单模式屏蔽IP地址
- 屏蔽DNS的开关
三个接口功能不同,先创建对应的三个规则子链;然后在子链中添加规则,实现具体功能。netd和framework同步增加接口,一一对应。所以增加的接口如下所示:
oneway void makeSeaiceChildChain(); //创建子链 oneway void setIdleType(); //清空规则 oneway void setBlackTypeIpList(in @utf8InCpp String addr); //黑名单模式 oneway void setWhiteTypeIpList(in @utf8InCpp String addr); //白名单模式 oneway void setBlockDns(boolean enable); //屏蔽查询DNS与否
实现接口
接口的实现主要就是添加iptables规则,如何添加可以参考FirewallController现成的代码(@makeUidRules)。接口实现在下面这个文件:
netd\server\OemNetdListener.cpp
makeSeaiceChildChain的实现代码如下,它是在启动的时候调用一次,可以在NetworkManagementServer的systemReady调用,创建出三个子链。
::android::binder::Status OemNetdListener::makeSeaiceChildChain() {
int res = -1;
IptablesTarget target = V4V6;
std::string command = "*filter\n";
std::vector<std::string> seaiceChains;
//子链
seaiceChains = {WHITE_NAME_TYPE, BLACK_NAME_TYPE, REJECT_DNS_QUERY};
for(std::string& chain : seaiceChains) {
::android::base::StringAppendF(&command, "-N %s\n", chain.c_str());
}
::android::base::StringAppendF(&command, "COMMIT\n");
res = execIptablesRestore(target, command.c_str());
if(res == 0) {
return ::android::binder::Status::ok();
} else {
return ::android::binder::Status::fromServiceSpecificError(res,
::android::String8::format("makeSeaiceChildChain error: %d", res));
}
}
setBlackTypeIpList接口的实现代码如下:
::android::binder::Status OemNetdListener::setBlackTypeIpList(const std::string& addr) {
std::lock_guard lock(mMutex);
//check chain exist or not
int res = -1;
IptablesTarget target = V4V6;
std::string command = "*filter\n";
::android::base::StringAppendF(&command, "-C OUTPUT -j %s\n", BLACK_NAME_TYPE);
::android::base::StringAppendF(&command, "COMMIT\n");
res = execIptablesRestore(target, command.c_str());
//clean chain
command = "*filter\n";
if (!res) {
::android::base::StringAppendF(&command, "-D OUTPUT -j %s\n", BLACK_NAME_TYPE);
}
::android::base::StringAppendF(&command, "-F %s\n", BLACK_NAME_TYPE);
::android::base::StringAppendF(&command, "-A OUTPUT -j %s\n", BLACK_NAME_TYPE);
std::stringstream ss(addr);
std::string ipAddr;
while(std::getline(ss, ipAddr, '-')) {
::android::base::StringAppendF(&command, "-A %s -d %s -j DROP\n", BLACK_NAME_TYPE, ipAddr.c_str());
}
::android::base::StringAppendF(&command, "-A %s -p all -j RETURN\n", BLACK_NAME_TYPE);
::android::base::StringAppendF(&command, "COMMIT\n");
res = execIptablesRestore(target, command.c_str());
if(res == 0) {
return ::android::binder::Status::ok();
} else {
return ::android::binder::Status::fromServiceSpecificError(res,
::android::String8::format("makeSeaiceChildChain error: %d", res));
}
}
其他接口的实现不再一一列举。
编写APK
NetworkManagementService的接口并不对外公开,不是标准的SDK API,所以需要编写一个需要系统权限的APK。这里主要涉及如何编译一个具有系统权限的APK。参考其他系统APK,主要在于编写正确的android.bp, 然后通过系统编译。如下:
package {
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "packages_services_Telephony_license"
// to get the below license kinds:
// SPDX-license-identifier-Apache-2.0
// default_applicable_licenses: ["packages_services_Telephony_license"],
}
android_app {
name: "seaiceNetApp",
srcs: [
"java/**/*.java",
],
static_libs: [
"androidx-constraintlayout_constraintlayout",
"androidx.cardview_cardview",
"androidx.appcompat_appcompat",
],
//platform:平台的核心应用签名,签名的apk是完成系统的核心功能。
//这些apk所在的进程UID是system。manifest节点中有添加android:sharedUserId="android.uid.system"。
certificate: "platform",
//设置该标记后会使用sdk的hide的api來编译。编译的APK中使用了系统级API,必须设定该值。
//和Android.mk中的LOCAL_PRIVATE_PLATFORM_APIS的作用相同
platform_apis: true,
}
以上要主要理解和定义这两个值certificate和platform_apis,才能正确编译。其他的不打算贴代码了,都是一些UI上的逻辑。最后的效果如下所示:
参考文档
深入理解Android系统网络架构
Android 系统网络框架
Android 框架实现分析 - 网络 - Native层
Netd中的“netd”套接字
Android 策略路由
Android系统中iptables的应用(五)IdlertimerController
Android 6.0 StrictController
Android 10 路由添加原理
Android系统签名简介
About LOCAL_PRIVATE_PLATFORM_APIS in Android.mk
针对非 SDK 接口的限制
Android中的 eBPF 流量监控
速查 | Android 系统目录功能
Android Doze and App Standby模式详解
重看ebpf -代码载入执行点-hook