最新消息:

手动创建ICMP Tunnel实现VPN上网(附Python实现代码) 转载

技术相关 admin 3905浏览

其实这是一篇讲解利用中国移动CMWAP的一些特性来实现免费上网的博文,但是没有以这个为标题,因为
1、用的是2G的EDGE网络,跟2G手机上网一样,速度有限.
2、各地区的移动设备有差别,例如在广州,这种方法行不通。但是广州的移动Wifi是可以通过UDP建立VPN来免费使用的。
3、不想吸引太多人的注意。

我见过很多公共网络都对UDP和TCP有不少限制,以致于我们不能自由地访问互联网。为了摆脱这种束缚,很多人都为此付出了很多努力,例如各种代理软件,各种隧道,各种VPN等。本文介绍的是一种比较罕见的ICMP隧道方式建立VPN。
背景
一年前,因为在县城里没有网络使用,又不想晚上跑到外面上,所以经常使用笔记本连接手机的GPRS网络来上网。打开一些网页或者聊天工具之类的,网 速的快慢并不是很重要。但是流量有限,当时开通了300MB的套餐也很快被消耗尽了。有一次,在Ubuntu下创建了一个cmwap网络,cmwap是需 要设置代理IP为10.0.0.172才能上网的。但是奇怪的是,我竟然可以ping通我自己博客服务器的IP。所以我想cmwap对外网IP的访问只是 限制了TCP和UDP类型的数据包进出而已。

为了验证我的想法是否正确,我在网上查找到了一些现成的Ping Tunnel工具,例如ptunnel。ptunnel至今还可以使用,但是问题很多,

1、ptunnel只支持TCP。
2、我使用了ptunnel之后,我ping不了自己的服务器了。显然ptunnel截获了所有ping包。(或者我用的版本太低?)
3、速度不稳定,可能是因为自己实现的可靠协议不是很完善?
用cmwap连接后,我使用ptunnel从我博客上下载一个5MB大小的文件。然后用手机查询流量,发现没有少。
由此说明,移动的cmwap是不计算ping的流量的。同时可以大胆猜测,流量计费功能应该是在10.0.0.172的代理服务器上进行的。这就是为何访问移动的一些服务(例如飞信网站)不会收流量费的原因。
我在国内似乎还没有搜索到有文章关于这方面的介绍。同时我在广州用cmwap连接后,无法发送ping包,难道是这个漏洞修补了?有兴趣的朋友可以 在自己的地区测试一下。先把连接方式更改为cmwap,然后连上后,ping一下8.8.8.8,看是否收到pong!!!如果能收到,恭喜!你可以利用 此特性免费上网。(另外,移动的路由很多,每次连接后得到的IP都不一样,路由也不一定一样。多换几次IP,可以得到能够ping的路由。——2012年 1月29日补充)
我发现这个问题后本来打算发博文的,后来不知道为何忘记了。只是偶尔跟身边一些朋友说了一下。
原理
回到主题上!
前段时间使用UDP隧道建立了一个VPN,把我学校里的一个VPS跟美国的VPS接入了同一个网络。参考了这篇文章《SSH_VPN》,有兴趣的同学可以看一下如何使用系统工具手动建立一个VPN。
这次要做的是,让从笔记本输出的IP包,使用ICMP协议封装后,像ping包一样发送到我的服务器,然后我的服务器解除ICMP的封装,把得到的 IP包写入本地路由。接着,把捕获到的发送给笔记本的IP包,使用ICMP封装,像pong包(ping的reply)一样发送到我的笔记本。到达笔记本 之后,接触ICMP封装,把得到的IP包写入本地路由。这样就在两个机器之间建立了点对点网络了。在此基础上,使用ip,iptables命令设置一下规 则,就建立了一个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收到了回应。
很详细吧!!!完整的工作流程就是这样。但关键需要解决的是封装ip包和解除封装。
步骤
需要解决以下一些问题:
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

    view plaincopy to clipboardprint?


    #!/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

    view plaincopy to clipboardprint?


    #!/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   = 0x0001  
      
    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@xiaoxia-pc:~/project/icmptun# ./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@xiaoxia-pc:~/project/icmptun# 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@xiaoxia-pc:~/project/icmptun# 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@xiaoxia-pc:~/project/icmptun# 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@xiaoxia-pc:~/project/icmptun# 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来摆脱网络限制。
我使用中的抓包,

Wireshark很好用,在我分析ICMP的过程中,帮助很大。当然在服务器上使用tcpdump -n icmp来查看ICMP是否工作也很有用。
184.22.224.213是我的VPS上IP之一。

现在已经夜深了,EDGE的速度还行吧。
root@xiaoxia-pc:~/project/icmptun# wget http://xiaoxia.org/upfiles/a.zip
–2012-01-16 04:22:54– http://xiaoxia.org/upfiles/a.zip
正在解析主机 xiaoxia.org… 184.22.224.213
正在连接 xiaoxia.org|184.22.224.213|:80… 已连接。
已发出 HTTP 请求,正在等待回应… 200 OK
长度: 262194 (256K) [application/zip]
正在保存至: “a.zip.1”

100%[============================================>] 262,194 18.8K/s 花时 14s

2012-01-16 04:23:09 (18.5 KB/s) – 已保存 “a.zip.1” [262194/262194])

本文主要用于学习交流,欢迎与大家共同探讨!!!
附本文所用代码文件下载:
icmptun.tar

转载请注明:Kermit的网站 » 手动创建ICMP Tunnel实现VPN上网(附Python实现代码) 转载