udpip: 用UDP封装IP数据包建立VPN

原理

使用Linux内核提供的tun设备建立可以在脚本读写的虚拟网卡,然后通过UDP将两个网卡的数据连接。


此方法能够使用以下特殊环境下:

1、客户端所在网络的路由不支持ppp,或者网络受到限制
2、TCP数据包被劫持或者受到限制
3、服务器是OpenVZ等不支持建立pptp,像我的burst的VPS就是这样子。

使用

服务器:

# python udptun.py -s 86 -l 10.0.0.1/24
Configuring interface t0 with ip 10.0.0.1/24

客户端:

# python udptun.py -c xiaoxia.org,86 -l 10.0.0.2/24
Configuring interface t0 with ip 10.0.0.2/24
Setting up new gateway …
Do login …
Logged in server succefully!

脚本代码

udptun.py:

#!/usr/bin/python

'''
    UDP Tunnel VPN
    Xiaoxia (xiaoxia@xiaoxia.org)
    Updated: 2012-2-21
'''

import os, sys
import hashlib
import getopt
import fcntl
import time
import struct
import socket, select
import traceback
import signal
import ctypes
import binascii

SHARED_PASSWORD = hashlib.sha1("xiaoxia").digest()
TUNSETIFF = 0x400454ca
IFF_TUN   = 0x0001

BUFFER_SIZE = 8192
MODE = 0
DEBUG = 0
PORT = 0
IFACE_IP = "10.0.0.1/24"
MTU = 1500
TIMEOUT = 60*10 # seconds

class Tunnel():
    def create(self):
        try:
            self.tfd = os.open("/dev/net/tun", os.O_RDWR)
        except:
            self.tfd = os.open("/dev/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):
        print "Configuring interface %s with ip %s" % (self.tname, 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 config_routes(self):
        if MODE == 1: # Server
            pass
        else: # Client
            print "Setting up new gateway ..."
            # Look for default route
            routes = os.popen("ip route show").readlines()
            defaults = [x.rstrip() for x in routes if x.startswith("default")]
            if not defaults:
                raise Exception("Default route not found, maybe not connected!")
            self.prev_gateway = defaults[0]
            self.prev_gateway_metric = self.prev_gateway + " metric 2"
            self.new_gateway = "default dev %s metric 1" % (self.tname)
            self.tun_gateway = self.prev_gateway.replace("default", IP)
            self.old_dns = file("/etc/resolv.conf", "rb").read()
            # Remove default gateway
            os.system("ip route del " + self.prev_gateway)
            # Add default gateway with metric
            os.system("ip route add " + self.prev_gateway_metric)
            # Add exception for server
            os.system("ip route add " + self.tun_gateway)
            # Add new default gateway
            os.system("ip route add " + self.new_gateway)
            # Set new DNS to 8.8.8.8
            file("/etc/resolv.conf", "wb").write("nameserver 8.8.8.8")
            
    def restore_routes(self):
        if MODE == 1: # Server
            pass
        else: # Client
            print "Restoring previous gateway ..."
            os.system("ip route del " + self.new_gateway)
            os.system("ip route del " + self.prev_gateway_metric)
            os.system("ip route del " + self.tun_gateway)
            os.system("ip route add " + self.prev_gateway)
            file("/etc/resolv.conf", "wb").write(self.old_dns)
    
    def run(self):
        global PORT
        self.udpfd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        if MODE == 1:
            self.udpfd.bind(("", PORT))
        else:
            self.udpfd.bind(("", 0))
            
        self.clients = {}
        self.logged = False
        self.try_logins = 5
        self.log_time = 0
            
        while True:
            if MODE == 2 and not self.logged and time.time() - self.log_time > 2.:
                print "Do login ..."
                self.udpfd.sendto("LOGIN:" + SHARED_PASSWORD + ":" + 
                    IFACE_IP.split("/")[0], (IP, PORT))
                self.try_logins -= 1
                if self.try_logins == 0:
                    raise Exception("Failed to log in server.")
                self.log_time = time.time()
                
            rset = select.select([self.udpfd, self.tfd], [], [], 1)[0]
            for r in rset:
                if r == self.tfd:
                    if DEBUG: os.write(1, ">")
                    data = os.read(self.tfd, MTU)
                    if MODE == 1: # Server
                        src, dst = data[16:20], data[20:24]
                        for key in self.clients:
                            if dst == self.clients[key]["localIPn"]:
                                self.udpfd.sendto(data, key)
                        # Remove timeout clients
                        curTime = time.time()
                        for key in self.clients.keys():
                            if curTime - self.clients[key]["aliveTime"] > TIMEOUT:
                                print "Remove timeout client", key
                                del self.clients[key]
                    else: # Client
                        self.udpfd.sendto(data, (IP, PORT))
                elif r == self.udpfd:
                    if DEBUG: os.write(1, "<")
                    data, src = self.udpfd.recvfrom(BUFFER_SIZE)
                    if MODE == 1: # Server
                        key = src
                        if key not in self.clients:
                            # New client comes
                            try:
                                if data.startswith("LOGIN:") and data.split(":")[1]==SHARED_PASSWORD:
                                    localIP = data.split(":")[2]
                                    self.clients[key] = {"aliveTime": time.time(), 
                                                        "localIPn": socket.inet_aton(localIP)}
                                    print "New Client from", src, "request IP", localIP
                                    self.udpfd.sendto("LOGIN:SUCCESS", src)
                            except:
                                print "Need valid password from", src
                                self.udpfd.sendto("LOGIN:PASSWORD", src)
                        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("LOGIN"):
                            if data.endswith("PASSWORD"):
                                self.logged = False
                                print "Need password to login!"
                            elif data.endswith("SUCCESS"):
                                self.logged = True
                                self.try_logins = 5
                                print "Logged in server succefully!"
                        else:
                            os.write(self.tfd, data)

        
def usage(status = 0):
    print "Usage: %s [-s port|-c serverip] [-hd] [-l localip]" % (sys.argv[0])
    sys.exit(status)

def on_exit(no, info):
    raise Exception("TERM signal caught!")

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
            PORT = int(optarg)
        elif opt == "-c":
            MODE = 2
            IP, PORT = optarg.split(",")
            IP = socket.gethostbyname(IP)
            PORT = int(PORT)
        elif opt == "-l":
            IFACE_IP = optarg
    
    if MODE == 0 or PORT == 0:
        usage(1)
    
    tun = Tunnel()
    tun.create()
    tun.config(IFACE_IP)
    signal.signal(signal.SIGTERM, on_exit)
    tun.config_routes()
    try:
        tun.run()
    except KeyboardInterrupt:
        pass
    except:
        print traceback.format_exc()
    finally:
        tun.restore_routes()
        tun.close()

udpip: 用UDP封装IP数据包建立VPN》上有43条评论

  1. onion254

    Hi您好,看了您的几篇文章,有个关于QQ2011协议的问题想请教一下:我用QQ2011正式版5074,wireshark抓包,其中有些包显示的协议是OICQ,Data部分前5位是这样的,02 27 35 00 17,02就不说了,27 35是版本,第4位和第5位00 17是所执行的操作,例如00 17后面给的注释就是 Command: Receive message (23),00 23 是Command: Get friend online (39)等等。问题来了。。OICQ的包里,command能看见,但是udp的就看不见了。。。我想问下您知不知道udp包里的data部分第四位和第五位到底是怎么个规律呢?比如发送视频可能是01 c0,00 15是查找好友之类的?想知道有没有全一点的关于这种特征的规律呢?

    回复
    1. Xiaoxia 文章作者

      你可以上网看看别人分析的文章,应该会对你很有帮助。
      对于登录协议的分析,应该有很多人都做过的。

      回复
    1. Xiaoxia 文章作者

      没试过openvpn么?
      我是自己不会搭建openvpn,才自己写了个脚本来做vpn,o(∩∩)o…哈哈。

      回复
  2. Pingback引用通告: udpip: 用UDP封装IP数据包建立VPN « 细节的力量

  3. ...

    hi,看了你之前的ICMP Tunnel,在vps上运行脚本后都提示

    tun.create()
    File “udptun.py”, line 38, in create
    self.tfd = os.open(“/dev/tun”, os.O_RDWR)
    OSError: [Errno 2] No such file or directory: ‘/dev/tun’
    我谁否缺了什么东西?

    回复
    1. Xiaoxia 文章作者

      你的vps是openvz的吗?
      如果是的话,需要有tun设备的权限的。如果是burst的vps,需要在VPS的管理面板的选项“Enable Tun/Tap”打开tun。

      这个问题网上应该有很多解决方案。

      回复
    1. Xiaoxia 文章作者

      可以呀,使用长密钥的XOR,可以保证数据的安全性 :)
      我想,以后增强这个VPN脚本的时候,会加入这个简单的加密功能。
      目前G。F。W还不会窥探UDP的数据,除了DNS数据包。

      回复
  4. fish

    hi.怎么我把你的代码保存为py文件后,在本地windows机器下,双击该py文件,弹出一个dos窗口,闪了一下就消失了?我的windows机器装了python-2.7.3

    回复
    1. Xiaoxia 文章作者

      你可以先打开控制台,然后在控制台里输入py文件的路径来执行脚本,出错信息会显示出来。

      回复
      1. fish

        hi.我运行了c:\python27\python.exe,然后在该dos窗口中输入i:\udptun.py后,回车,显示:
        File “”,line 1
        SyntaxError: invalid syntax

        难道你的代码哪里写错了?

        回复
          1. fish

            hi.
            我操作成功,terminal里的提示跟你文章一样,不过建立连接后,还是翻不了墙阿,怎么回事?

            回复
  5. Pingback引用通告: 隧道:IP隧道原理剖析与实现 | 白杨 baiyang

  6. scola

    xiaoxia你好,请教一个问题,我想把以上的客户端做成支持多客户端,
    只建立一个tun interface可以同时连接多客户端么?
    还是每个客户端都需要建立一个tun 接口。

    麻烦了,等待你的回复。

    回复
    1. Xiaoxia 文章作者

      不需要多个tun的,你可能搞错了。负责连接多客户端的,应该是程序。程序使用的虚拟网卡,只要一个就够。不然,这些客户端怎么互相通信呢

      回复
      1. scola

        虾哥,现在我倒是能理解一个tun设备就能做到多客户端连接,
        但是我运行你的这个程序,可以稳定上网大约半小时(稳定多长时间也不固定)
        之后客户端就收不到udp包,终止客户端重新运行客户端又会正常一小会儿。之后又收不到回包
        >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
        从调试上看,只发包,没有回包,server 上发包,回包都有,应该正常。
        我用tcpdump看了下,也是客户端没有回包。和调试信息反应的一样
        我家长城宽带连着无线路由上网,vps使用的亚马逊ec2的免费一年那个。
        只连接了一个客户端(一台debian)。
        虾哥能简单分析下原因么,如何提高稳定性,先谢谢了

        回复
          1. scola

            谢谢虾哥凌晨的回复,好感动,早睡保重身体哈。
            我今天将mtu改成1400和1000,调小了,出现收不到回包的次数少了,但是还是会出现收不到回包的现象。
            原因可能不是程序的问题。

            回复
  7. max

    程序可在Xen Linux执行,不过路由的设定与恢复貌似把路由顺序搞反了。
    max@max-K43SV:.bin$ route -n
    Kernel IP routing table
    Destination Gateway Genmask Flags Metric Ref Use Iface
    0.0.0.0 192.168.1.1 0.0.0.0 UG 0 0 0 eth0
    169.254.0.0 0.0.0.0 255.255.0.0 U 1000 0 0 eth0
    192.168.1.0 0.0.0.0 255.255.255.0 U 1 0 0 eth0
    max@max-K43SV:.bin$ route -n
    Kernel IP routing table
    Destination Gateway Genmask Flags Metric Ref Use Iface
    0.0.0.0 0.0.0.0 0.0.0.0 U 1 0 0 t0
    0.0.0.0 192.168.1.1 0.0.0.0 UG 2 0 0 eth0
    10.0.0.0 0.0.0.0 255.255.255.0 U 0 0 0 t0
    169.254.0.0 0.0.0.0 255.255.0.0 U 1000 0 0 eth0
    xxx.xxx.xx.xx 192.168.1.1 255.255.255.255 UGH 0 0 0 eth0
    192.168.1.0 0.0.0.0 255.255.255.0 U 1 0 0 eth0

    回复
    1. max

      不好意思啊,看错了,原来Ubuntu的路由表顺序和CentOS是相反的,学linux的时候是用CentOS虚拟机,平时用是用ubuntu … 囧…

      回复
  8. Zenkio

    在windows 上测试您的程序,fcntl import不了,我换了 tornado的 win32_support.py ,但提示没有/dev/tun或/dev/net/tun,当然因为windows这个器件是linux的。请问在windows上可以怎么改呢?

    回复

发表评论

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

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>