【Android】 使用VPN实现抓包

VPN抓包

使用VPN技术可以直接获得网络三层IP报文,可以不可以基于此实现移动端抓包呢?肯定可以,Android已经有大批开源库基于该思路实现抓包。我们来学习下原理。

VpnService

VpnService是开发Android VPN的基础,下面是官方文档的阐释

VpnService is a base class for applications to extend and build their own VPN solutions. In general, it creates a virtual network interface, configures addresses and routing rules, and returns a file descriptor to the application. Each read from the descriptor retrieves an outgoing packet which was routed to the interface. Each write to the descriptor injects an incoming packet just like it was received from the interface. The interface is running on Internet Protocol (IP), so packets are always started with IP headers. The application then completes a VPN connection by processing and exchanging packets with the remote server over a tunnel.

上面的阐释的重点是:

  • 虚拟一个网卡
  • 返回文件描述符
  • 读写的内容是ip数据报

首先推荐Android官方提供了的Example:ToyVpn。这个例子比较简单,实操一遍可以帮助理解VPN原理。

初步搭一个vpn应用框架也可以参考这篇文章,这个仅仅是搭建了框架,功能(ip数据包的收发)则没有实现。

源码分析

Android系统提供的API是VpnService,我们调用establish()方法之后会返回一个FD。然后我们读取该FD就能获得ip数据报文,通过发往VPN Server,再获得Server的回复报文之后再写入该FD,就可以实现VPN通信。

VpnService#establish()

[-> frameworks/base/core/java/android/net/VpnService.java]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/**
* Create a VPN interface using the parameters supplied to this
* builder. The interface works on IP packets, and a file descriptor
* is returned for the application to access them. Each read
* retrieves an outgoing packet which was routed to the interface.
* Each write injects an incoming packet just like it was received
* from the interface. The file descriptor is put into non-blocking
* mode by default to avoid blocking Java threads. To use the file
* descriptor completely in native space, see
* {@link ParcelFileDescriptor#detachFd()}. The application MUST
* close the file descriptor when the VPN connection is terminated.
* The VPN interface will be removed and the network will be
* restored by the system automatically.
*
* <p>To avoid conflicts, there can be only one active VPN interface
* at the same time. Usually network parameters are never changed
* during the lifetime of a VPN connection. It is also common for an
* application to create a new file descriptor after closing the
* previous one. However, it is rare but not impossible to have two
* interfaces while performing a seamless handover. In this case, the
* old interface will be deactivated when the new one is created
* successfully. Both file descriptors are valid but now outgoing
* packets will be routed to the new interface. Therefore, after
* draining the old file descriptor, the application MUST close it
* and start using the new file descriptor. If the new interface
* cannot be created, the existing interface and its file descriptor
* remain untouched.
*
* <p>An exception will be thrown if the interface cannot be created
* for any reason. However, this method returns {@code null} if the
* application is not prepared or is revoked. This helps solve
* possible race conditions between other VPN applications.
*
* @return {@link ParcelFileDescriptor} of the VPN interface, or
* {@code null} if the application is not prepared.
* @throws IllegalArgumentException if a parameter is not accepted
* by the operating system.
* @throws IllegalStateException if a parameter cannot be applied
* by the operating system.
* @throws SecurityException if the service is not properly declared
* in {@code AndroidManifest.xml}.
* @see VpnService
*/
@Nullable
public ParcelFileDescriptor establish() {
mConfig.addresses = mAddresses;
mConfig.routes = mRoutes;

try {
return getService().establishVpn(mConfig);
} catch (RemoteException e) {
throw new IllegalStateException(e);
}
}


/**
* Use IConnectivityManager since those methods are hidden and not
* available in ConnectivityManager.
*/
private static IConnectivityManager getService() {
return IConnectivityManager.Stub.asInterface(
ServiceManager.getService(Context.CONNECTIVITY_SERVICE));
}

其实是调用了ConnectivityService 这个系统服务的 establishVpn(mConfig)方法。

ConnectivityService#establishVpn()

[-> frameworks/base/services/core/java/com/android/server/ConnectivityService.java]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected final SparseArray<Vpn> mVpns = new SparseArray<>();

/**
* Configure a TUN interface and return its file descriptor. Parameters
* are encoded and opaque to this class. This method is used by VpnBuilder
* and not available in ConnectivityManager. Permissions are checked in
* Vpn class.
* @hide
*/
@Override
public ParcelFileDescriptor establishVpn(VpnConfig config) {
int user = UserHandle.getUserId(Binder.getCallingUid());
synchronized (mVpns) {
throwIfLockdownEnabled();
// mVpns其实是个Vpn数组
return mVpns.get(user).establish(config);
}
}

Vpn#establish()

[-> frameworks/base/services/core/java/com/android/server/connectivity/Vpn.java]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
/**
* Establish a VPN network and return the file descriptor of the VPN interface. This methods
* returns {@code null} if the application is revoked or not prepared.
*
* @param config The parameters to configure the network.
* @return The file descriptor of the VPN interface.
*/
public synchronized ParcelFileDescriptor establish(VpnConfig config) {
// Check if the caller is already prepared.
UserManager mgr = UserManager.get(mContext);
if (Binder.getCallingUid() != mOwnerUID) {
return null;
}
// Check to ensure consent hasn't been revoked since we were prepared.
if (!isVpnUserPreConsented(mPackage)) {
return null;
}
// Check if the service is properly declared.
Intent intent = new Intent(VpnConfig.SERVICE_INTERFACE);
intent.setClassName(mPackage, config.user);
long token = Binder.clearCallingIdentity();
try {
// Restricted users are not allowed to create VPNs, they are tied to Owner
UserInfo user = mgr.getUserInfo(mUserHandle);
if (user.isRestricted()) {
throw new SecurityException("Restricted users cannot establish VPNs");
}

ResolveInfo info = AppGlobals.getPackageManager().resolveService(intent,
null, 0, mUserHandle);
if (info == null) {
throw new SecurityException("Cannot find " + config.user);
}
if (!BIND_VPN_SERVICE.equals(info.serviceInfo.permission)) {
throw new SecurityException(config.user + " does not require " + BIND_VPN_SERVICE);
}
} catch (RemoteException e) {
throw new SecurityException("Cannot find " + config.user);
} finally {
Binder.restoreCallingIdentity(token);
}

// Save the old config in case we need to go back.
VpnConfig oldConfig = mConfig;
String oldInterface = mInterface;
Connection oldConnection = mConnection;
NetworkAgent oldNetworkAgent = mNetworkAgent;
Set<UidRange> oldUsers = mNetworkCapabilities.getUids();

// 调用jniCreate方法创建了一个FD
// Configure the interface. Abort if any of these steps fails.
ParcelFileDescriptor tun = ParcelFileDescriptor.adoptFd(jniCreate(config.mtu));
try {
String interfaze = jniGetName(tun.getFd());

// TEMP use the old jni calls until there is support for netd address setting
StringBuilder builder = new StringBuilder();
for (LinkAddress address : config.addresses) {
builder.append(" ");
builder.append(address);
}
if (jniSetAddresses(interfaze, builder.toString()) < 1) {
throw new IllegalArgumentException("At least one address must be specified");
}
Connection connection = new Connection();
if (!mContext.bindServiceAsUser(intent, connection,
Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
new UserHandle(mUserHandle))) {
throw new IllegalStateException("Cannot bind " + config.user);
}

mConnection = connection;
mInterface = interfaze;

// Fill more values.
config.user = mPackage;
config.interfaze = mInterface;
config.startTime = SystemClock.elapsedRealtime();
mConfig = config;

// Set up forwarding and DNS rules.
// First attempt to do a seamless handover that only changes the interface name and
// parameters. If that fails, disconnect.
if (oldConfig != null
&& updateLinkPropertiesInPlaceIfPossible(mNetworkAgent, oldConfig)) {
// Keep mNetworkAgent unchanged
} else {
mNetworkAgent = null;
updateState(DetailedState.CONNECTING, "establish");
// Set up forwarding and DNS rules.
agentConnect();
// Remove the old tun's user forwarding rules
// The new tun's user rules have already been added above so they will take over
// as rules are deleted. This prevents data leakage as the rules are moved over.
agentDisconnect(oldNetworkAgent);
}

if (oldConnection != null) {
mContext.unbindService(oldConnection);
}

if (oldInterface != null && !oldInterface.equals(interfaze)) {
jniReset(oldInterface);
}

try {
IoUtils.setBlocking(tun.getFileDescriptor(), config.blocking);
} catch (IOException e) {
throw new IllegalStateException(
"Cannot set tunnel's fd as blocking=" + config.blocking, e);
}
} catch (RuntimeException e) {
IoUtils.closeQuietly(tun);
// If this is not seamless handover, disconnect partially-established network when error
// occurs.
if (oldNetworkAgent != mNetworkAgent) {
agentDisconnect();
}
// restore old state
mConfig = oldConfig;
mConnection = oldConnection;
mNetworkCapabilities.setUids(oldUsers);
mNetworkAgent = oldNetworkAgent;
mInterface = oldInterface;
throw e;
}
Log.i(TAG, "Established by " + config.user + " on " + mInterface);
return tun;
}

establish()方法除了权限校验之外,主要是通过jniCreate方法创建了一个FD并返回,这个FD其实就是tun设备。而此处的jniCreate方法是native方法,如下:

1
2
3
4
5
6
7
private native int jniCreate(int mtu);
private native String jniGetName(int tun);
private native int jniSetAddresses(String interfaze, String addresses);
private native void jniReset(String interfaze);
private native int jniCheck(String interfaze);
private native boolean jniAddAddress(String interfaze, String address, int prefixLen);
private native boolean jniDelAddress(String interfaze, String address, int prefixLen);

可以看到一系列的native方法,这些方法都位于com_android_server_connectivity_Vpn.cpp中:

jniCreate()

[-> frameworks/base/services/core/jni/com_android_server_connectivity_Vpn.cpp]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
static const JNINativeMethod gMethods[] = {
{"jniCreate", "(I)I", (void *)create},
{"jniGetName", "(I)Ljava/lang/String;", (void *)getName},
{"jniSetAddresses", "(Ljava/lang/String;Ljava/lang/String;)I", (void *)setAddresses},
{"jniReset", "(Ljava/lang/String;)V", (void *)reset},
{"jniCheck", "(Ljava/lang/String;)I", (void *)check},
{"jniAddAddress", "(Ljava/lang/String;Ljava/lang/String;I)Z", (void *)addAddress},
{"jniDelAddress", "(Ljava/lang/String;Ljava/lang/String;I)Z", (void *)delAddress},
};

// 创建tun设备
static jint create(JNIEnv *env, jobject /* thiz */, jint mtu)
{
int tun = create_interface(mtu);
if (tun < 0) {
throwException(env, tun, "Cannot create interface");
return -1;
}
return tun;
}


static int create_interface(int mtu)
{
int tun = open("/dev/tun", O_RDWR | O_NONBLOCK | O_CLOEXEC);

ifreq ifr4;
memset(&ifr4, 0, sizeof(ifr4));

// Allocate interface.
ifr4.ifr_flags = IFF_TUN | IFF_NO_PI;
if (ioctl(tun, TUNSETIFF, &ifr4)) {
ALOGE("Cannot allocate TUN: %s", strerror(errno));
goto error;
}

// Activate interface.
ifr4.ifr_flags = IFF_UP;
if (ioctl(inet4, SIOCSIFFLAGS, &ifr4)) {
ALOGE("Cannot activate %s: %s", ifr4.ifr_name, strerror(errno));
goto error;
}

// Set MTU if it is specified.
ifr4.ifr_mtu = mtu;
if (mtu > 0 && ioctl(inet4, SIOCSIFMTU, &ifr4)) {
ALOGE("Cannot set MTU on %s: %s", ifr4.ifr_name, strerror(errno));
goto error;
}

return tun;

error:
close(tun);
return SYSTEM_ERROR;
}

这里就很明显能看到最终调用的是create_interface方法,该方法其实就是打开/dev/tun文件,然后设置一下MTU并返回这个FD。

/dev/tun文件就是Linux中的TUN设备。

TUN/TAP是什么

tap/tun 是 Linux 内核 2.4.x 版本之后实现的虚拟网络设备,不同于物理网卡,tap/tun 虚拟网卡完全由软件来实现,功能和硬件实现完全没有差别,它们都属于网络设备,都可以配置 IP,都归 Linux 网络设备管理模块统一管理。
TAP 设备与 TUN 设备都是虚拟网络设备,工作方式完全相同,但它们的工作层次不太一样:

  • tap 是一个二层设备(或者以太网设备),只能处理二层的以太网帧;
  • tun 是一个点对点的三层设备(或网络层设备),只能处理三层的 IP 数据包。

作为网络设备,tap/tun 也需要配套相应的驱动程序才能工作。tap/tun 驱动程序包括两个部分,一个是字符设备驱动,一个是网卡驱动。这两部分驱动程序分工不太一样,字符驱动负责数据包在内核空间和用户空间的传送,网卡驱动负责数据包在 TCP/IP 网络协议栈上的传输和处理。

字符驱动 - 内核空间和用户空间的数据传输

在 Linux 中,用户空间和内核空间的数据传输有多种方式,字符设备就是其中的一种。tap/tun 通过驱动程序和一个与之关联的字符设备,来实现用户空间和内核空间的通信接口。

在 Linux 内核 2.6.x 之后的版本中,tap/tun 对应的字符设备文件分别为:

  • tap:/dev/tap0
  • tun:/dev/tun0

设备文件即充当了用户空间和内核空间通信的接口。当应用程序打开设备文件时,驱动程序就会创建并注册相应的虚拟设备接口,一般以 tunXtapX 命名。当应用程序关闭文件时,驱动也会自动删除 tunXtapX 设备,还会删除已经建立起来的路由等信息。

tap/tun 设备文件就像一个管道,一端连接着用户空间,一端连接着内核空间。当用户程序向文件 /dev/net/tun/dev/tap0 写数据时,内核就可以从对应的 tunXtapX 接口读到数据,反之,内核可以通过相反的方式向用户程序发送数据。

网卡驱动 - 数据包在 TCP/IP 协议栈的传输

参考网络虚拟化技术(二): TUN/TAP MACVLAN MACVTAP,先来看看物理网卡是如何工作的:

真实网卡的工作机制

普通网卡通过网线收发数据包,所有物理网卡收到的包会交给内核的 Network Stack 处理,然后通过 Socket API 通知给用户程序。

但是 TUN 设备通过一个/dev/tunX文件收发数据包。所有对该文件的写操作会通过 TUN 设备转换成一个数据包送给内核;当内核发送一个包给 TUN 设备时,通过读这个文件可以拿到包的内容。

如果我们使用 TUN 设备搭建一个基于 UDP VPN,那么整个处理过程就是这样:

基于 UDP 的 VPN 工作机制

数据包会通过内核网络栈两次。但是经过 App 的处理后,数据包可能已经加密,并且原有的 ip 头被封装在 udp 内部,所以第二次通过网络栈内核看到的添加了新的IP头和UDP头的数据包。

第二次进入网络栈内核后的IP数据包变化

tap/tun 通过实现相应的网卡驱动程序来和网络协议栈通信。一般的流程和物理网卡和协议栈的交互流程是一样的,不同的是物理网卡一端是连接物理网络,而 tap/tun 虚拟网卡一般连接到用户空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
+----------------------------------------------------------------+
| |
| +--------------------+ +--------------------+ |
| | User Application A | | User Application B |<-----+ |
| +--------------------+ +--------------------+ | |
| | 1 | 5 | |
|...............|......................|...................|.....|
| ↓ ↓ | |
| +----------+ +----------+ | |
| | socket A | | socket B | | |
| +----------+ +----------+ | |
| | 2 | 6 | |
|.................|.................|......................|.....|
| ↓ ↓ | |
| +------------------------+ 4 | |
| | Newwork Protocol Stack | | |
| +------------------------+ | |
| | 7 | 3 | |
|................|...................|.....................|.....|
| ↓ ↓ | |
| +----------------+ +----------------+ | |
| | eth0 | | tun0 | | |
| +----------------+ +----------------+ | |
| 10.32.0.11 | | 192.168.3.11 | |
| | 8 +---------------------+ |
| | |
+----------------|-----------------------------------------------+

Physical Network

上图中有两个应用程序A和B,都在用户层,而其它的socket、协议栈(Newwork Protocol Stack)和网络设备(eth0和tun0)部分都在内核层,其实socket是协议栈的一部分,这里分开来的目的是为了看的更直观。

tun0是一个Tun/Tap虚拟设备,从上图中可以看出它和物理设备eth0的差别,它们的一端虽然都连着协议栈,但另一端不一样,eth0的另一端是物理网络,这个物理网络可能就是一个交换机,而tun0的另一端是一个用户层的程序,协议栈发给tun0的数据包能被这个应用程序读取到,并且应用程序能直接向tun0写数据。

这里假设eth0配置的IP是10.32.0.11,而tun0配置的IP是192.168.3.11.

这里列举的是一个典型的tun/tap设备的应用场景,发到192.168.3.0/24网络的数据通过程序B这个隧道,利用10.32.0.11发到远端网络的10.33.0.1,再由10.33.0.1转发给相应的设备,从而实现VPN。

下面来看看数据包的流程:

  1. 应用程序A是一个普通的程序,通过socket A发送了一个数据包,假设这个数据包的目的IP地址是192.168.3.1
  2. socket将这个数据包丢给协议栈
  3. 协议栈根据数据包的目的IP地址,匹配本地路由规则,知道这个数据包应该由tun0出去,于是将数据包交给tun0
  4. tun0收到数据包之后,发现另一端被进程B打开了,于是将数据包丢给了进程B
  5. 进程B收到数据包之后,做一些跟业务相关的处理,然后构造一个新的数据包,将原来的数据包嵌入在新的数据包中,最后通过socket B将数据包转发出去,这时候新数据包的源地址变成了eth0的地址,而目的IP地址变成了一个其它的地址,比如是10.33.0.1.
  6. socket B将数据包丢给协议栈
  7. 协议栈根据本地路由,发现这个数据包应该要通过eth0发送出去,于是将数据包交给eth0
  8. eth0通过物理网络将数据包发送出去

10.33.0.1收到数据包之后,会打开数据包,读取里面的原始数据包,并转发给本地的192.168.3.1,然后等收到192.168.3.1的应答后,再构造新的应答包,并将原始应答包封装在里面,再由原路径返回给应用程序B,应用程序B取出里面的原始应答包,最后返回给应用程序A

这里不讨论Tun/Tap设备tun0是怎么和用户层的进程B进行通信的,对于Linux内核来说,有很多种办法来让内核空间和用户空间的进程交换数据。

从上面的流程中可以看出,数据包选择走哪个网络设备完全由路由表控制,所以如果我们想让某些网络流量走应用程序B的转发流程,就需要配置路由表让这部分数据走tun0。

根据Android VpnService初探

  • 什么IP包会往虚拟网卡发?或者说,虚拟网卡出入的IP报文怎么才算有效的?
    • 只有本地到外地,或者外地到本地的报文,才会途径虚拟网卡。
    • 比如说, 本地6666端口,往本地9999端口发送了一个UDP包,其对应的IP报文不会经过虚拟网卡,不要指望读ParcelFileDescriptor实例能够得到这份IP报文。
    • 比如说, 本地6666端口,往外地8.8.8.8:53发送了一个UDP包。虚拟网卡收到后,将其目的地改为指向本地UDPServer6.6.6.6:6666,重新写入虚拟网卡。
      但这时本地UDPServer并不会收到这个报文,只有将这个IP报文的源地址更改一下,例如由本地ip改为8.8.8.8(原目的地址),这个报文才会被本地UDPServer收到。
  • 关于DNS报文
    • 似乎如果不在Builder生成时指定DNS服务器,手机本身另有一套机制,使得系统默认的DNS查询不会经过虚拟网卡。如果有需要,不妨再加一条:
      builder.addDnsServer("114.114.114.114")

这个讨论也很有信息量:VpnService 能否原样将三层的 IP 报文发出去?

参考资料

Powered by Hexo and Hexo-theme-hiker

Copyright © 2013 - 2019 iTimeTraveler All Rights Reserved.

访客数 : | 访问量 :