作为通信系统的一部分,也出于其他原因,本实验室需要研究一种代理程序。 本研究的基本设想是将数据通过多路方式无序地传送到对方服务器,然后自动重组, 建立一个在两个服务器之间的连接。

为了达到这个目的,我们需要至少建立一个能将TCP连接的通信传递到服务器上的隧道。 这样就有了本文将讨论的问题:我们如何获取和传递被代理的连接中的数据?

设想1: 编写一个TCP的监听服务器,将从TCP传递的数据流接收,然后拆分,通过多个渠道送出去,然后在目标服务器上重组。

这样做的问题是,通过多个渠道发送的数据包,在目标的顺序很可能被打乱。数据包也可能丢失。 为了解决这个问题,一个复杂的,用来解决重组数据包、重发丢失的数据包的程序就是必要的。 但是,TCP协议本来就是有类似这样的功能的。TCP协议处理下层的数据包的时候, 并不要求数据包按顺序到达,也不要求数据包没有丢失。 对于这些问题的处理,都是TCP协议的任务。相反,UDP方式传送数据的时候,就不能保证这一点:传送的数据可能丢失,也可能无序。

所以我们这样做,将是在重新制造轮子(reinvent the wheel),是个很复杂的事情。

设想2: 既然TCP数据包下层的IP数据包,是可以无序、无保证传送的,那么如果我们编写IP数据包的代理呢?

这应该是可行的!TCP现在就有多路TCP的概念。VPN也是基于这个原理实现的。 如果我们将IP数据包收集起来,用UDP包加密承载它们,就应该能做到一个代理服务器的事情。

本文就将谈一谈,如何将IP数据包转换成UDP封包,构建一个简单无加密的VPN。

需要的材料和工具

首先,为了模拟一个VPN,笔者建议建立两台计算机。

其次,这两台计算机可以位于互联网或者局域网,能通过UDP方式直接联系到对方。

两台计算机上都需要安装socat这个命令。在Ubuntu操作系统下,可以通过apt-get install socat安装。

原理

TUN/TAP设备

达成这个目的的关键是Linux的TUN/TAP设备。

内核中的TUN/TAP驱动程序在/dev/net/tun或者/dev/tun这里有个特殊设备。 当这个文件被一个程序打开的时候,内核就会给系统添加一块虚拟网卡,例如tun0

说网卡是虚拟的,是说,这块网卡没有任何与之对应的物理线路。

想象一个真实的网卡: 任何时候,当内核认为这块网卡上的数据包需要通过网线发送的时候,对于TUN/TAP设备,数据包就会直接发送给建立虚拟网卡的这个程序。 反过来,如果这个程序向/dev/net/tun写入数据包,内核就会把这个数据包当作是从网线传来的数据包处理。

如果我们编写这样一个程序,能收发本地计算机上的虚拟网卡数据包, 然后直接原封不动,传递给另一台计算机上的接收程序,让接收程序把数据包写入另一台计算机上的虚拟网卡, 那么就完成了一个连接两台计算机的隧道,好像两台计算机通过一根虚拟的网线相互连接。 这就是本代理程序的原理。

这种隧道有TUN和TAP两种模式建立。TUN模式中,程序收发的是网卡中的IP数据帧。TAP模式则更底层一点,收发的是以太网帧。 本文由于知识局限,只使用第一种模式实验。

socat命令

socat是一个很强大的命令,可以在两个数据源之间建立双向的连接。

socat命令的基本用法很简单,就是socat <SRC1> <SRC2>。 这样,由SRC1SRC2定义的两个数据源之间就可以互相通信。 用man socat命令可以查到很多数据源的定义。

例如:使用这个命令将例如标准输入输出发送到一个远端的TCP口:

socat - TCP:www.google.com:80

然后输入

GET /

回车,就会看到www.google.com返回类似下文所见的结果(结果经过编辑并非原始数据):

HTTP/1.0 302 Found
Cache-Control: private
Content-Type: text/html; charset=UTF-8
Location: http://www.google.com/
Content-Length: 219
Date: Sun, 23 Aug 2015 16:00:00 GMT
Server: GFE/2.0

<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>302 Moved</TITLE></HEAD><BODY>
<H1>302 Moved</H1>
The document has moved
<A HREF="http://www.google.com/">here</A>.
</BODY></HTML>

实验

实验1: 利用socat命令构建一个简单的VPN

首先登录两台计算机,然后启动命令行。下面的命令需要root权限完成。

首先确定两台计算机的防火墙不会阻挡UDP连接。如果有ufw命令的话,可以用ufw disable暂时关闭防火墙。

我们假设连接到对方计算机时所用的对方IP地址是1.2.3.4

首先在对方计算机上运行:

socat UDP-LISTEN:5555 TUN:10.0.0.2/24,up

这样就在对方计算机上建立了一个TUN设备,并为之分配了虚拟IP地址10.0.0.2,子网掩码是255.255.255.0/24)。 这个设备的通信和一个在5555监听的UDP端口相连。

在本地计算机上我们运行:

socat UDP:1.2.3.4:5555 TUN:10.0.0.1/24,up

建立一个虚拟网卡,地址是10.0.0.1,在这个网卡上收发的包发送到对方计算机的5555端口。

两个命令执行后,两台计算机就好象是位于10.0.0.0/24局域网中了。

首先我们用ping来检验这个连接,在本地计算机上:

ping 10.0.0.2

应当能收到回复。我们还可以访问对方计算机的程序,例如,如果对方计算机上运行了HTTP服务,可以用浏览器打开http://10.0.0.1进行访问。

如果对方计算机上运行了代理服务器,那么,通过10.0.0.2这个IP地址,也可以访问到对方计算机的代理服务。 这样我们不需要修改路由表,也可以通过这个代理程序访问互联网了。

实验2: 用Python编写一个监听TUN数据包的程序

首先需要安装一个Python的模块python-pytun

pip install python-pytun

这个模块提供了比较简单的方法来为系统添加一个TUN设备。示范程序如下:

from pytun import TunTapDevice
from select import select

tun1 = TunTapDevice()
tun2 = TunTapDevice()

print "Name %s" % tun1.name
tun1.addr = '10.8.0.1'
tun1.dstaddr = '10.8.0.2'
tun1.netmask = '255.255.255.0'
tun1.mtu = 1500
tun1.up()

print "Name %s" % tun2.name
tun2.addr = '10.8.0.2'
tun2.dstaddr = '10.8.0.1'
tun2.netmask = '255.255.255.0'
tun2.mtu = 1500
tun2.up()

while True:
    r = select([tun1, tun2], [], [])[0][0]
    try:
        buf = r.read(r.mtu)
        if r == tun1:
            read = tun1.name
            tun2.write(buf)
        else:
            read = tun2.name
            tun1.write(buf)
        print "Read from %s: %s" % (read, buf.encode('hex'))
    except:
        tun1.close()
        tun2.close()
        exit()

使用root权限运行这个程序。将会在本计算机添加两块虚拟网卡。

在命令行使用ping 10.8.0.100,可以看到ping所用的ICMP数据包应该出现。

如果ping 10.8.0.2或者ping 10.8.0.1,不会看到数据包,因为系统内核知道这两块虚拟网卡在本地计算机,所以不会通过虚拟网线发送数据。