Android app 指定网络发送数据包的实现与原理分析
APP 和 native 使用的说明
App 使用默认网络
在android 中 一个app使用网络,需要在manifest 申请一下
<uses-permission android:name="android.permission.INTERNET"/>
这种方式将使用default网络,比如WIFI 和 数据网络,android 同一个时间点,只能有一个default网络(这个默认网络实质是什么或者怎么实现的,后面分析netd的文章会写)
那有没有一种方式可以不使用默认网络呢,比如使用一个专有的网络通道访问专有的服务,答案是有的
APP使用指定网络
- 通过 requestNetwork 申请一个网络
- 在NetworkCallback中的onAvailable的方法去调用bindProcessToNetwork 去bind这个网络
- 上两步后APP的网络流量将会走这个network,或者说走这个network 指定的 网卡
补充说明一下 :NetworkRequest 在CS对应一个NetworkRequestInfo ,一般情况下一个NetworkRequestInfo对应了一个client进程
一个例子
NetworkRequest request = new NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.build();
mNetworkCallback = new ConnectivityManager.NetworkCallback() {
@Override
public void onAvailable(final Network network) {
runOnUiThread(new Runnable() {
@Override
public void run() {
// requires android.permission.INTERNET
if (!mConnectivityManager.bindProcessToNetwork(network)) {
} else {
Log.d(TAG, "Network available");
}
}
});
}
Native进程使用指定网络
- #include “NetdClient.h” 此文件,此文件在netd的源码中,并动态链接libnetd_client.so ,注意一定是动态链接,
- 调用 setNetworkForProcess()
- 强调一下,一定是动态链接
原理分析
申请网络requestNetwork
//frameworks/base/core/java/android/net/ConnectivityManager.java
public void requestNetwork(NetworkRequest request, NetworkCallback networkCallback) {
}
- NetworkRequest 可以设置 TransportType 比如 TRANSPORT_CELLULAR或者 TRANSPORT_WIFI
- NetworkRequest 可以设置NetworkCapabilities比如NET_CAPABILITY_INTERNET或者其他类型
这个方法可能导致一个新的Network的出现,对应ConnectivityService中就是一个NetworkAgentInfo,关于这一点另一篇文章会详细的说一下,这里可以简单的认为一个NetworkAgentInfo代表一个网络通道
NetworkCallback 里面有一些回调,说明一下
绑定网络 BindProcessToNetwork
public static boolean setProcessDefaultNetwork(Network network) {
int netId = (network == null) ? NETID_UNSET : network.netId;
if (netId == NetworkUtils.getBoundNetworkForProcess()) {
return true;
}
if (NetworkUtils.bindProcessToNetwork(netId)) {}
}
BindProcessToNetwork底层原理分析
bindProcessToNetwork ,这个方法通过jni的方式调用了libnetd_client.so
用network中netId作为mark 设置到socket,导致app socket的数据包打上此mark,这个mark将会匹配到android的策略路由中,走到network对应网卡的路由表中.
例如network 的netId =101
adb 查看策略路由
0: from all lookup local
9000: from all lookup main
10000: from all fwmark 0xc0000/0xd0000 lookup legacy_system
10500: from all oif dummy0 uidrange 0-0 lookup dummy0
10500: from all oif rmnet_data1 uidrange 0-0 lookup rmnet_data1
10500: from all oif rmnet_data0 uidrange 0-0 lookup rmnet_data0
10500: from all oif p2p0 uidrange 0-0 lookup local_network
13000: from all fwmark 0x10063/0x1ffff lookup local_network
13000: from all fwmark 0x10066/0x1ffff lookup rmnet_data1
13000: from all fwmark 0x10065/0x1ffff lookup rmnet_data0
注意这个 0x10065 ,就是101的16进制,就是说设置netid 101 mark的数据包会走到这条策略路由,进而通过rmnet_data网卡发送数据
问题思考
为什么调用了setNetworkForProcess ,之后app不管采用何种方式的访问网络,比如okhttp 或者HttpURLConnection的原生方式都能路由到特定的网卡上呢
不管采用方式,本质都使用了socket的,最终都会调用到sys/socket.h的socket(c库)
bionic/libc/include/sys/socket.h
#include <sys/socket.h> 代码使用
int socket(int domain, int type, int protocol) {
return __netdClientDispatch.socket(domain, type, protocol);
}
__netdClientDispatch.socket 最初会被赋值为__socket(int, int, int);
extern "C" __socketcall int __socket(int, int, int);
在__libc_preinit_impl 的时候会通过dlsym的方式调用/system/lib/libnetd_client.so中的netdClientSocket(前面说的要动态链接的原因)
extern "C" void netdClientInitSocket(SocketFunctionType* function) {
if (function && *function) {
libcSocket = *function;
*function = netdClientSocket;
}}
netdClientInitSocket 执行后会使得 __netdClientDispatch.socket 被赋值为netdClientSocket 而libcSocket赋值为__scoket(系统调用)
Android app 和 native 创建的socket最终会调用到netClientSocket
int netdClientSocket(int domain, int type, int protocol) {
int socketFd;
#ifndef USE_WRAPPER
// 系统调用得到一个标准的socket
socketFd = libcSocket(domain, type, protocol);
#else
if( __propClientDispatch.propSocket ) {
socketFd = __propClientDispatch.propSocket(domain, type, protocol);
} else {
socketFd = libcSocket(domain, type, protocol);
}
#endif
if (socketFd == -1) {
return -1;
}
unsigned netId = netIdForProcess;
// **将socket 打上 netId的mark**
if (netId != NETID_UNSET && FwmarkClient::shouldSetFwmark(domain)) {
if (int error = setNetworkForSocket(netId, socketFd)) {
return closeFdAndSetErrno(socketFd, error);
}
}
return socketFd;
}
在netdClientSocket创建的socket 会给socket打上netIdForProcess数值的mark,这个netIdForProcess其实就是bindProcessToNetwork 设置的netId,这样导致创建的socket都含有此mark,自然路由到netId对应的网卡了,hook的思想的体现!!
总结
再次感叹android的源码真牛逼,设计的如此巧妙,修改了linux的c库,通过hook的方式,在app 创建的socket自动打上mark,实现了数据包的路由!!