Android: VpnService与badvpn – tun2socks & udpgw

Android SDK提供了android.net.VpnService类,使得我们可以很方便地实现自定义的VPN客户端解决方案。一般来说,VpnService用来创建一个虚拟网络接口,配置接口的IP地址,路由规则。在建立VPN客户端之后,应用程序会取得一个文件描述符,从这个文件描述符中可以 读取由这个虚拟网络接口转发过来的数据包(retrieves an outging packet), 同时也可以往这个文件描述符中写入数据包(injects an incoming packet) 。由于接口(tun0)运行在Internet Protocol(IP)层,所以每个数据包都是以IP头开始的。

  • 环境

Android版本: android-5.1.1_r15。代码在项目中编译。

  • ToyVpn

在Android项目中, 提供了一个sample app: development/samples/ToyVpn,演示如何实现一个简单的VPN客户端。

1. 服务器端

相关的代码在development/samples/ToyVpn/server/linux/ToyVpnServer.cpp,并包含一个Makefile文件,在ubuntu14.04 x86_64系统下,输入make可以很方便地生成服务器端程序ToyVpnServer。

参考ToyVpnServer.cpp中的步聚,搭建一个VPN服务器:

// There are several ways to play with this program. Here we just give an
// example for the simplest scenario. Let us say that a Linux box has a
// public IPv4 address on eth0. Please try the following steps and adjust
// the parameters when necessary.
//
// # Enable IP forwarding
// echo 1 > /proc/sys/net/ipv4/ip_forward
//
// # Pick a range of private addresses and perform NAT over eth0.
// iptables -t nat -A POSTROUTING -s 10.0.0.0/8 -o eth0 -j MASQUERADE
//
// # Create a TUN interface.
// ip tuntap add dev tun0 mode tun
//
// # Set the addresses and bring up the interface.
// ifconfig tun0 10.0.0.1 dstaddr 10.0.0.2 up
//
// # Create a server on port 8000 with shared secret "test".
// ./ToyVpnServer tun0 8000 test -m 1400 -a 10.0.0.2 32 -d 8.8.8.8 -r 0.0.0.0 0
//
// This program only handles a session at a time. To allow multiple sessions,
// multiple servers can be created on the same port, but each of them requires
// its own TUN interface. A short shell script will be sufficient. Since this
// program is designed for demonstration purpose, it performs neither strong
// authentication nor encryption. DO NOT USE IT IN PRODUCTION!

这里需要注意tun0的IP地址不要与其他网络的IP地址在同一个网络。

2. 客户端

相关的代码在development/examples/ToyVpn/src/com/example/android/toyvpn/ToyVpnService.java中。

从服务器端获取配置信息:mtu size, ip address, dns address和route,之后就通过VpnService.Builder建立VPN连接,取得文件描述符:

public class ToyVpnService extends VpnService implements Handler.Callback, Runnable {
    // ...
    private void configure(String parameters) throws Exception {
        // If the old interface has exactly the same parameters, use it!
        if (mInterface != null && parameters.equals(mParameters)) {
            Log.i(TAG, "Using the previous interface");
            return;
        }

        // Configure a builder while parsing the parameters.
        Builder builder = new Builder();
        for (String parameter : parameters.split(" ")) {
            String[] fields = parameter.split(",");
            try {
                switch (fields[0].charAt(0)) {
                    case 'm':
                        builder.setMtu(Short.parseShort(fields[1]));
                        break;
                    case 'a':
                        builder.addAddress(fields[1], Integer.parseInt(fields[2]));
                        break;
                    case 'r':
                        builder.addRoute(fields[1], Integer.parseInt(fields[2]));
                        break;
                    case 'd':
                        builder.addDnsServer(fields[1]);
                        break;
                    case 's':
                        builder.addSearchDomain(fields[1]);
                        break;
                }
            } catch (Exception e) {
                throw new IllegalArgumentException("Bad parameter: " + parameter);
            }
        }

        // Close the old interface since the parameters have been changed.
        try {
            mInterface.close();
        } catch (Exception e) {
            // ignore
        }

        // Create a new interface using the builder and save the parameters.
        mInterface = builder.setSession(mServerAddress)
                .setConfigureIntent(mConfigureIntent)
                .establish();
        mParameters = parameters;
        Log.i(TAG, "New interface: " + parameters);
    }
}

3. VpnService类中几个比较重要的方法

protect(int socket)/protect(DatagramSocket socket)/protect(Socket socket): 当调用这个方法之后,从这个socket进出的数据会直接走underlying network:

Protect a socket from VPN connections. After protecting, data sent through this socket will go directly to the underlying network, so its traffic will not be forwarded through the VPN. This method is useful if some connections need to be kept outside of VPN. For example, a VPN tunnel should protect itself if its destination is covered by VPN routes. Otherwise its outgoing packets will be sent back to the VPN interface and cause an infinite loop. This method will fail if the application is not prepared or is revoked.

调用VpnService.Builder.establish()得到的是一个ParcelFileDescriptor对象,如果native代码要使用这个fd, 可以调用ParcelFileDescriptor.detachFd()这个方法:

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 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.

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.

  • badvpn – tun2socks

tun2socks将来自TUN设备的所有TCP连接请求forward到SOCKS server。

相关的源代码可以从这里下载:

https://github.com/ambrop72/badvpn

由badvpn/tun2socks/badvpn-tun2socks.8可知,tun2socks的用法如下:

This example demonstrates using tun2socks in combination with SSH's dynamic forwarding feature.
Connect to the SSH server, passing -D localhost:1080 to the ssh command to enable dynamic  forwarding.  This will make ssh open a local SOCKS server which tun2socks forward connection through.
First create a TUN device (eg. using openvpn):

    openvpn --mktun --dev tun0 --user <someuser>

Configure the IP of the new tun device:

     ifconfig tun0 10.0.0.1 netmask 255.255.255.0

Now start the badvpn-tun2socks program:

     badvpn-tun2socks --tundev tun0 --netif-ipaddr 10.0.0.2 --netif-netmask 255.255.255.0 \
                      --socks-server-addr 127.0.0.1:1080

Note  that  the address 10.0.0.2 is not a typo. It specifies the IP address of the virtual router inside the TUN device, and must be different from the IP of the TUN interface itself (but in the same subnet).

Now you should be able to ping the virtual router's IP (10.0.0.2):

     ping -n 10.0.0.2

由此可知,在Android系统中,要使用tun2socks,需要能够访问TUN设备,而App是没有权限直接访问这个设备的。好在tun2socks提供了相关的函数支持fd操作,只需稍作改动:

diff --git a/tun2socks/tun2socks.c b/tun2socks/tun2socks.c
index 748b8c5..0b6a34f 100644
--- a/tun2socks/tun2socks.c
+++ b/tun2socks/tun2socks.c
@@ -99,6 +99,8 @@ struct {
     int loglevel;
     int loglevels[BLOG_NUM_CHANNELS];
     char *tundev;
+    int tunfd;
+    int tunmtu;
     char *netif_ipaddr;
     char *netif_netmask;
     char *netif_ip6addr;
@@ -327,12 +329,28 @@ int main (int argc, char **argv)
         goto fail2;
     }
     
+#if 0
     // init TUN device
     if (!BTap_Init(&device, &ss, options.tundev, device_error_handler, NULL, 1)) {
         BLog(BLOG_ERROR, "BTap_Init failed");
         goto fail3;
     }
-    
+#else
+    // init TUN fd
+    {
+        struct BTap_init_data init_data;
+        init_data.dev_type = BTAP_DEV_TUN;
+        init_data.init_type = BTAP_INIT_FD;
+        init_data.init.fd.fd = options.tunfd;
+        init_data.init.fd.mtu = options.tunmtu;
+
+        if (!BTap_Init2(&device, &ss, init_data, device_error_handler, NULL)) {
+            BLog(BLOG_ERROR, "BTap_Init2 failed");
+            goto fail3;
+        }
+    }
+#endif
+
     // NOTE: the order of the following is important:
     // first device writing must evaluate,
     // then lwip (so it can send packets to the device),
@@ -483,6 +501,8 @@ void print_help (const char *name)
         "        [--loglevel <0-5/none/error/warning/notice/info/debug>]\n"
         "        [--channel-loglevel <channel-name> <0-5/none/error/warning/notice/info/debug>] ...\n"
         "        [--tundev <name>]\n"
+        "        [--tunfd <number>]\n"
+        "        [--tunmtu <number>]\n"
         "        --netif-ipaddr <ipaddr>\n"
         "        --netif-netmask <ipnetmask>\n"
         "        --socks-server-addr <addr>\n"
@@ -523,6 +543,8 @@ int parse_arguments (int argc, char *argv[])
         options.loglevels[i] = -1;
     }
     options.tundev = NULL;
+    options.tunfd = -1;
+    options.tunmtu = -1;
     options.netif_ipaddr = NULL;
     options.netif_netmask = NULL;
     options.netif_ip6addr = NULL;
@@ -620,6 +642,22 @@ int parse_arguments (int argc, char *argv[])
             options.tundev = argv[i + 1];
             i++;
         }
+        else if (!strcmp(arg, "--tunfd")) {
+            if (1 >= argc - i) {
+                fprintf(stderr, "%s: requires an argument\n", arg);
+                return 0;
+            }
+            options.tunfd = atoi(argv[i + 1]);
+            i++;
+        }
+        else if (!strcmp(arg, "--tunmtu")) {
+            if (1 >= argc - i) {
+                fprintf(stderr, "%s: requires an argument\n", arg);
+                return 0;
+            }
+            options.tunmtu = atoi(argv[i + 1]);
+            i++;
+        }
         else if (!strcmp(arg, "--netif-ipaddr")) {
             if (1 >= argc - i) {
                 fprintf(stderr, "%s: requires an argument\n", arg);
  • badvpn – udpgw

相关改动如下:

diff --git a/udpgw/udpgw.c b/udpgw/udpgw.c
index 9c6a341..870faa1 100644
--- a/udpgw/udpgw.c
+++ b/udpgw/udpgw.c
@@ -60,6 +60,7 @@
 
 #ifndef BADVPN_USE_WINAPI
 #include <base/BLog_syslog.h>
+#include <arpa/inet.h>
 #include <arpa/nameser.h>
 #include <resolv.h>
 #endif
@@ -140,6 +141,7 @@ struct {
     int local_udp_ip6_num_ports;
     char *local_udp_ip6_addr;
     int unique_local_ports;
+    char *dns_addr;
 } options;
 
 // MTUs
@@ -367,6 +369,7 @@ void print_help (const char *name)
         "        [--local-udp-addrs <addr> <num_ports>]\n"
         "        [--local-udp-ip6-addrs <addr> <num_ports>]\n"
         "        [--unique-local-ports]\n"
+        "        [--dns-addr <addr>]\n"
         "Address format is a.b.c.d:port (IPv4) or [addr]:port (IPv6).\n",
         name
     );
@@ -563,6 +566,10 @@ int parse_arguments (int argc, char *argv[])
         else if (!strcmp(arg, "--unique-local-ports")) {
             options.unique_local_ports = 1;
         }
+        else if (!strcmp(arg, "--dns-addr")) {
+            options.dns_addr = arg;
+            i++;
+        }
         else {
             fprintf(stderr, "unknown option: %s\n", arg);
             return 0;
@@ -1437,6 +1444,7 @@ int uint16_comparator (void *unused, uint16_t *v1, uint16_t *v2)
 
 void maybe_update_dns (void)
 {
+#if 0
 #ifndef BADVPN_USE_WINAPI
     btime_t now = btime_gettime();
     if (now < btime_add(last_dns_update_time, DNS_UPDATE_TIME)) {
@@ -1470,4 +1478,9 @@ void maybe_update_dns (void)
 fail:
     BAddr_InitNone(&dns_addr);
 #endif
+#else
+    BAddr addr;
+    BAddr_InitIPv4(&addr, inet_addr(options.dns_addr), hton16(53));
+    dns_addr = addr;
+#endif
  • badvpn – 代码及编译脚本

当前最新代码的版本为:

commit 61a68b25878dbf48d45df064b2e8215e48db45ce
Author: Ambroz Bizjak <ambrop7@gmail.com>
Date:   Sun Jan 10 17:04:48 2016 +0100

    Fix tun2socks compile script (missing source file).

如下Android.mk文件可以编译出Nexus4设备可用的tun2socks, udpgw可执行文件:

LOCAL_PATH := $(call my-dir)

my_badvpn_C_INCLUDES := \
	$(LOCAL_PATH)/lwip/custom \
	$(LOCAL_PATH)/lwip/src/include/ipv4 \
	$(LOCAL_PATH)/lwip/src/include/ipv6 \
	$(LOCAL_PATH)/lwip/src/include

my_badvpn_CFLAGS := \
	-DBADVPN_THREADWORK_USE_PTHREAD -DBADVPN_LINUX -DBADVPN_BREACTOR_BADVPN \
	-D_GNU_SOURCE -DBADVPN_USE_SELFPIPE -DBADVPN_USE_EPOLL \
	-DBADVPN_LITTLE_ENDIAN -DBADVPN_THREAD_SAFE

my_badvpn_CFLAGS += \
	-std=gnu99 -Wno-unused-parameter -Wno-sign-compare

include $(CLEAR_VARS)

LOCAL_MODULE := libbadvpn
LOCAL_MODULE_TAGS := optional

LOCAL_SRC_FILES := \
	base/BLog.c \
	base/BLog_syslog.c \
	base/BPending.c \
	base/DebugObject.c \
	flow/BufferWriter.c \
	flowextra/PacketPassInactivityMonitor.c \
	flow/PacketBuffer.c \
	flow/PacketPassConnector.c \
	flow/PacketPassFairQueue.c \
	flow/PacketPassInterface.c \
	flow/PacketProtoDecoder.c \
	flow/PacketProtoEncoder.c \
	flow/PacketProtoFlow.c \
	flow/PacketRecvInterface.c \
	flow/PacketStreamSender.c \
	flow/SinglePacketBuffer.c \
	flow/StreamPassInterface.c \
	flow/StreamRecvInterface.c \
	lwip/custom/sys.c \
	lwip/src/core/def.c \
	lwip/src/core/inet_chksum.c \
	lwip/src/core/init.c \
	lwip/src/core/ipv4/autoip.c \
	lwip/src/core/ipv4/icmp.c \
	lwip/src/core/ipv4/igmp.c \
	lwip/src/core/ipv4/ip4_addr.c \
	lwip/src/core/ipv4/ip4.c \
	lwip/src/core/ipv4/ip_frag.c \
	lwip/src/core/ipv6/dhcp6.c \
	lwip/src/core/ipv6/ethip6.c \
	lwip/src/core/ipv6/icmp6.c \
	lwip/src/core/ipv6/inet6.c \
	lwip/src/core/ipv6/ip6_addr.c \
	lwip/src/core/ipv6/ip6.c \
	lwip/src/core/ipv6/ip6_frag.c \
	lwip/src/core/ipv6/mld6.c \
	lwip/src/core/ipv6/nd6.c \
	lwip/src/core/mem.c \
	lwip/src/core/memp.c \
	lwip/src/core/netif.c \
	lwip/src/core/pbuf.c \
	lwip/src/core/stats.c \
	lwip/src/core/tcp.c \
	lwip/src/core/tcp_in.c \
	lwip/src/core/tcp_out.c \
	lwip/src/core/timers.c \
	lwip/src/core/udp.c \
	socksclient/BSocksClient.c \
	system/BConnection_common.c \
	system/BConnection_unix.c \
	system/BDatagram_unix.c \
	system/BNetwork.c \
	system/BReactor_badvpn.c \
	system/BSignal.c \
	system/BTime.c \
	system/BUnixSignal.c \
	tuntap/BTap.c \
	udpgw_client/UdpGwClient.c

LOCAL_C_INCLUDES := $(my_badvpn_C_INCLUDES)
LOCAL_CFLAGS := $(my_badvpn_CFLAGS)

include $(BUILD_STATIC_LIBRARY)

include $(CLEAR_VARS)

LOCAL_MODULE := badvpn-tun2socks
LOCAL_MODULE_TAGS := optional
LOCAL_UNINSTALLABLE_MODULE := true

LOCAL_SRC_FILES := \
	tun2socks/SocksUdpGwClient.c \
	tun2socks/tun2socks.c

LOCAL_C_INCLUDES := $(my_badvpn_C_INCLUDES)
LOCAL_CFLAGS := $(my_badvpn_CFLAGS)
LOCAL_STATIC_LIBRARIES := libbadvpn

include $(BUILD_EXECUTABLE)

include $(CLEAR_VARS)

LOCAL_MODULE := badvpn-udpgw
LOCAL_MODULE_TAGS := optional
LOCAL_UNINSTALLABLE_MODULE := true

LOCAL_SRC_FILES := \
	udpgw/udpgw.c

LOCAL_C_INCLUDES := $(my_badvpn_C_INCLUDES)
LOCAL_CFLAGS := $(my_badvpn_CFLAGS)
LOCAL_STATIC_LIBRARIES := libbadvpn

include $(BUILD_EXECUTABLE)
  • 相关的参考文档:
  1. http://developer.android.com/reference/android/net/VpnService.html
  2. https://github.com/ambrop72/badvpn

《Android: VpnService与badvpn – tun2socks & udpgw》有1个想法

  1. 在toyvpn中的Android客户端里我需要输入(server IP,server port,shared secret,http proxy server,http proxy port,package)参数,而在server中需要使用./ToyVpnServer tun0 8000 -m 1400 -a 10.0.0.2 32 -d 8.8.8.8 -r 0.0.0.0 0。我想问的是客服端要输入什么才可以可服务器端进行链接呢?两者的参数分别如何对应呢?希望得到您的回复!感谢!

发表评论

电子邮件地址不会被公开。 必填项已用*标注