通过ICMP建立VPN加密隧道

原理,模拟一个网页请求流程:
1、Firefox在笔记本发出一个请求。
2、内核使用默认路由发送这个请求的IP包。
3、因为默认路由的设备设置的是tun0,所以被tunnel程序捕获。
4、tunnel程序读取ip包后,用icmp封装,发送到远程vps。
5、icmp无障碍地通过cmwap网络,发送到远程vps上。
6、远程vps收到后,被服务器端的tunnel程序捕获(tunnel程序捕获所有的icmp数据包)。
7、tunnel程序读取icmp包,获取里面的ip包,写入到本地网络中。
8、因为通过iptables设置了nat,所以该ip包的源地址被改为vps ip后,发送到了所请求的服务器上。
9、一个IP包从请求的服务器上返回到vps,经过nat后,进入tunnel程序建立的网络,被tunnel程序捕获。
10、tunnel程序读取ip包后,用icmp封装,发送到笔记本。
11、icmp回应包无障碍地通过cmwap网络,发送到笔记本上。
12、笔记本接收到该icmp包,被笔记本上的tunnel程序捕获。
13、tunnel程序读取icmp包,获取里面的ip包,写入到本地网络中。
14、内核得到这个ip包,通知指定的应用程序响应。
15、Firefox收到了回应。

步骤

需要解决以下一些问题:

1、如何捕获与发送icmp包

用socket的RAW模式即可。

2、如何不影响vps上正常的ping回应?

给icmp里的code字段设置一个固定值,默认是0,这个值可以随便设置。例如86。这样我们只捕获与发送code值为86的icmp数据包。跟普通的ping区别开来,互不影响。
同时,避免vps的内核回应我们的icmp包。添加下面的iptables规则。使用到–icmp-type type/code选项。type的值中,8是ping请求,0是ping响应,所以只针对响应包屏蔽。但是为了让服务器端的tunnel程序的icmp 能发出去,服务器端在发送的时候,可以把code+1,也就变为87,发送出去。

iptables -A OUTPUT -p icmp –icmp-type 0/86 -j DROP
了解更多关于ICMP的选项,请参见RFC792.

3、MTU问题

因为IP包被封装到ICMP里之后,体积肯定会变大,如果超出网络的MTU,内核就会用两个IP包来装。导致第一个IP包装满了,第二个IP包可能只有几十个字节。十分浪费。为了避免这种现象,可以设置虚拟网卡的mtu为1000或更少。

ip link set t0 mtu 1000

4、Python里处理ICMP

我自己写了一段代码,checksum的算法参考自 http://code.activestate.com/recipes/409689-icmplib-library-for-creating-and-reading-icmp-pack/

icmp.py

#!/usr/bin/env python
import socket
import binascii
import struct
import ctypes
BUFFER_SIZE = 8192
class IPPacket():
def _checksum(self, data):
if len(data) % 2:
odd_byte = ord(data[-1])
data = data[:-1]
else:
odd_byte = 0
words = struct.unpack(“!%sH” %(len(data)/2), data)
total = 0
for word in words:
total += word
else:
total += odd_byte
total = (total>>16) + (total & 0xffff)
total += total>>16
return ctypes.c_ushort(~total).value
def parse(self, buf, debug = True):
self.ttl, self.proto, self.chksum = struct.unpack(“!BBH”, buf[8:12])
self.src, self.dst = buf[12:16], buf[16:20]
if debug:
print ”parse IP ttl=”, self.ttl, ”proto=”, self.proto, ”src=”, socket.inet_ntoa(self.src),
”dst=”, socket.inet_ntoa(self.dst)
class ICMPPacket(IPPacket):
def parse(self, buf, debug = True):
IPPacket.parse(self, buf, debug)
self.type, self.code, self.chksum, self.id, self.seqno = struct.unpack(“!BBHHH”, buf[20:28])
if debug:
print ”parse ICMP type=”, self.type, ”code=”, self.code, ”id=”, self.id, ”seqno=”, self.seqno
return buf[28:]
def create(self, type_, code, id_, seqno, data):
packfmt = ”!BBHHH%ss” % (len(data))
args = [type_, code, 0, id_, seqno, data]
args[2] = IPPacket._checksum(self, struct.pack(packfmt, *args))
return struct.pack(packfmt, *args)

4、我写的Tunnel程序

实现了以下功能:

1)支持多人同时使用这个VPN。每个客户端通过外网IP与ICMP里的ID的组合来决定。所以,即使多个使用者在同一个局域网下,也不会相互使用。参见代码中key的计算。

2)使用密码登录以限制他人访问。初次连接服务器要求密码才能使用该VPN。默认为10分钟收不到来自客户端的数据包就删除会话。这个密码登录做的有点简单,当然只要稍加修改,让服务器返回一个随机字符串,客户端用密码跟这个随机字符串一起hash一下,就很无敌了。

3)服务器端和客户端共用一个tunnel程序。通过参数来指定工作模式。

tunnel.py

#!/usr/bin/env python
import os, sys
import hashlib
import getopt
import fcntl
import icmp
import time
import struct
import socket, select
SHARED_PASSWORD = hashlib.md5(“password”).digest()
TUNSETIFF = 0x400454ca
IFF_TUN   = 0×0001
MODE = 0
DEBUG = 0
PORT = 0
IFACE_IP = ”10.0.0.1″
MTU = 1500
CODE = 86
TIMEOUT = 60*10 # seconds
class Tunnel():
def create(self):
self.tfd = os.open(“/dev/net/tun”, os.O_RDWR)
ifs = fcntl.ioctl(self.tfd, TUNSETIFF, struct.pack(“16sH”, ”t%d”, IFF_TUN))
self.tname = ifs[:16].strip(“x00″)
def close(self):
os.close(self.tfd)
def config(self, ip):
os.system(“ip link set %s up” % (self.tname))
os.system(“ip link set %s mtu 1000″ % (self.tname))
os.system(“ip addr add %s dev %s” % (ip, self.tname))
def run(self):
self.icmpfd = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.getprotobyname(“icmp”))
self.clients = {}
packet = icmp.ICMPPacket()
self.client_seqno = 1
while True:
rset = select.select([self.icmpfd, self.tfd], [], [])[0]
for r in rset:
if r == self.tfd:
if DEBUG: os.write(1, ”>”)
data = os.read(self.tfd, MTU)
if MODE == 1: # Server
for key in self.clients:
buf = packet.create(0, CODE+1, self.clients[key]["id"], self.clients[key]["seqno"], data)
self.clients[key]["seqno"] += 1
self.icmpfd.sendto(buf, (self.clients[key]["ip"], 22))
# Remove timeout clients
curTime = time.time()
for key in self.clients.keys():
if curTime – self.clients[key]["aliveTime"] > TIMEOUT:
print ”Remove timeout client”, self.clients[key]["ip"]
del self.clients[key]
else: # Client
buf = packet.create(8, CODE, PORT, self.client_seqno, data)
self.client_seqno += 1
self.icmpfd.sendto(buf, (IP, 22))
elif r == self.icmpfd:
if DEBUG: os.write(1, ”<”)
buf = self.icmpfd.recv(icmp.BUFFER_SIZE)
data = packet.parse(buf, DEBUG)
ip = socket.inet_ntoa(packet.src)
if packet.code in (CODE, CODE+1):
if MODE == 1: # Server
key = struct.pack(“4sH”, packet.src, packet.id)
if key not in self.clients:
# New client comes
if data == SHARED_PASSWORD:
self.clients[key] = {“aliveTime”: time.time(),
”ip”: ip,
”id”: packet.id,
”seqno”: packet.seqno}
print ”New Client from %s:%d” % (ip, packet.id)
else:
print ”Wrong password from %s:%d” % (ip, packet.id)
buf = packet.create(0, CODE+1, packet.id, packet.seqno, ”PASSWORD”*10)
self.icmpfd.sendto(buf, (ip, 22))
else:
# Simply write the packet to local or forward them to other clients ???
os.write(self.tfd, data)
self.clients[key]["aliveTime"] = time.time()
else: # Client
if data.startswith(“PASSWORD”):
# Do login
buf = packet.create(8, CODE, packet.id, self.client_seqno, SHARED_PASSWORD)
self.client_seqno += 1
self.icmpfd.sendto(buf, (ip, 22))
else:
os.write(self.tfd, data)
def usage(status = 0):
print ”Usage: icmptun [-s code|-c serverip,code,id] [-hd] [-l localip]“
sys.exit(status)
if __name__==”__main__”:
opts = getopt.getopt(sys.argv[1:],”s:c:l:hd”)
for opt,optarg in opts[0]:
if opt == ”-h”:
usage()
elif opt == ”-d”:
DEBUG += 1
elif opt == ”-s”:
MODE = 1
CODE = int(optarg)
elif opt == ”-c”:
MODE = 2
IP,CODE,PORT = optarg.split(“,”)
CODE = int(CODE)
PORT = int(PORT)
elif opt == ”-l”:
IFACE_IP = optarg
if MODE == 0 or CODE == 0:
usage(1)
tun = Tunnel()
tun.create()
print ”Allocated interface %s” % (tun.tname)
tun.config(IFACE_IP)
try:
tun.run()
except KeyboardInterrupt:
tun.close()
sys.exit(0)

用法:

root@244754:~/lab/icmptun# ./tunnel.py
Usage: icmptun [-s code|-c serverip,code,id] [-hd] [-l localip]

5、VPS服务器端部署

把icmp.py和tunnel.py都copy到vps上去。注意要设置为可执行文件。然后用下面的命令来运行。

./tunnel.py -s 86 -l 10.1.2.1/24
Allocated interface t1

tunnel.py会创建一个虚拟网卡(tun)。上述命令中,虚拟网卡的IP为10.1.2.1,子网掩码为255.255.255.0。

查看已经建立的网卡,我这里显示为t1. 因为t0已经被我用作udp隧道。

root@244754:~/lab/icmptun# ip route show
184.22.224.0/24 dev venet0  proto kernel  scope link  src 184.22.224.212
10.1.1.0/24 dev t0  proto kernel  scope link  src 10.1.1.1
10.1.2.0/24 dev t1  proto kernel  scope link  src 10.1.2.1
default dev venet0  scope link

6、笔记本上客户端部署

以客户端模式启动tunnel.py,

root@aiobox.net:~/# ./tunnel.py -c 184.22.224.212,86,2012 -l 10.1.2.2/24
Allocated interface t0

-c的参数指定三项内容,用道号分隔,分别是远程服务器端的IP,发送ping时所使用的code,发送ping时所使用的id。code是区别普通的ping包,id是区别不同的客户端。

注意,如果在局域网环境下,经过网关后,这个id可能会变化,但不影响使用,因为回应包进入内网时,id会变回原值。

启动客户端后,在本地可以ping一下IP。

root@aiobox.net:~/# ping 10.1.2.2
PING 10.1.2.2 (10.1.2.2) 56(84) bytes of data.
64 bytes from 10.1.2.2: icmp_req=1 ttl=64 time=0.065 ms
64 bytes from 10.1.2.2: icmp_req=2 ttl=64 time=0.065 ms
64 bytes from 10.1.2.2: icmp_req=3 ttl=64 time=0.059 ms

很快就响应了,本地直接返回。

此时ping一下在vps的虚拟网卡,正常情况下,应该能得到回应了。

root@aiobox.net:~/# ping 10.1.2.1
PING 10.1.2.1 (10.1.2.1) 56(84) bytes of data.
64 bytes from 10.1.2.1: icmp_req=1 ttl=64 time=322 ms
64 bytes from 10.1.2.1: icmp_req=2 ttl=64 time=545 ms
64 bytes from 10.1.2.1: icmp_req=3 ttl=64 time=400 ms

到目前为止,已经在两台机器之间通过icmp建立了点对点隧道。

7、构建VPN

先在服务器端设置NAT。

iptables -t nat -A POSTROUTING -s 10.1.2.0/24 -j SNAT –to-source 184.22.224.212

再在本地设置路由表,让默认网关为新创建的t0. 同时注意把vps的ip设置为例外。

ip route add 184.22.224.212 via 10.64.64.64 dev ppp0
ip route del default
ip route add default dev t0

我的路由表如下:

root@aiobox.net:~/# ip route show
10.64.64.64 dev ppp0  proto kernel  scope link  src 10.134.75.35
184.22.224.212 via 10.64.64.64 dev ppp0
10.1.2.0/24 dev t0  proto kernel  scope link  src 10.1.2.2
169.254.0.0/16 dev ppp0  scope link  metric 1000
default dev t0  scope link

到此,已经可以通过t0访问网络了。

root@aiobox.net:~/# telnet www.google.com 80
Trying 203.208.46.180…
Connected to www.google.com.
Escape character is ‘^]’.

最后

可以把启动客户端以及设置路由的命令,写进一个脚本文件里,这样只需要一个命令,就能使用VPN了!像在我这个地方,就可以用这个工具实现在CMWAP的网络上免费上网,没有流量的限制。

当然,ICMP Tunnel在很多场合下都可以使用,只要ICMP没有被封,就有办法通过ICMP来建立隧道和VPN来摆脱网络限制。

相关工具:

http://www.cs.uit.no/~daniels/PingTunnel/

Social tagging: > >

发表评论