美文网首页
如何解决动态IP导致的VPN失效

如何解决动态IP导致的VPN失效

作者: c4a1d989518e | 来源:发表于2018-08-05 19:12 被阅读966次

    文章主要分三个部分,前面的偏基础,重点在最后第三条,是写的如何解决动态ip导致的一系列问题。有需要的话,可以直接到最后查看。

    想要实现的目标:

    1.通过公司内网的网段可以直接访问阿里云上的服务器主机,VPN 可以将一个机构的多个数据中心通过隧道的方式连接起来,让机构感觉像是在一个数据中心里面。
    2.在公司之外,无论在祖国各地,五湖四海,只要是能联网的地方,就能访问到公司内网。

    具体的实现

    1.打通公司内网到阿里云的VPN通道

    首先要说的就是通过公司内网访问阿里云上的服务器。


    IPsec连接

    这样说比较抽象,举个例子,如果我阿里云上的某台服务器的ip是188.100.100.1,那我要实现的效果是:

    ping 188.100.100.1
    

    能够ping通,当然首先要保证的是我这台服务器的ip和公司内网的网段不冲突,也就是说阿里云的vpc(Virtual Private Cloud可以理解为分配给服务器的私有网段)和公司内网正常办公的网段不冲突。

    在这里要注意的一点,是先设置路由器,然后在去阿里云上做设置。

    在阿里云中做这些设置:

    阿里云vpc中的vpn设置

    vpn网关:
    这里的vpn网关,一般指的是公司服务器集群总的网关,是一个固定IP作为网络的输入口,这里我们是不用新建设置的。注意这里的ip,是我们要用到的。

    vpn网关

    用户网关:
    用户网关这里的ip就是我们公司的外网ip,获得外网ip的方法有很多种,比如访问http://www.ip138.com/,这里需要我们新建创建用户网关来填入我们的ip。

    用户网关

    IPsec连接:
    这一步就是集大成的关键性一步了。

    IPsec连接
    这里的VPN网关和用户网关都选择我们上一步设置好的选择就好。这里的本端网段填写的是阿里云的VPC网段。而对端网段填写的是我们公司内网分配给设备上网的网段。
    新建A
    还有就是约定好喝公司的网关路由器上的共享秘钥。和加密算法之类的,保证云上和本地配置的一致。
    新建B

    重要的一点:如果你保证你配置的没问题,但还是提示‘第二阶段协商未成功’这样的错误。很可能是因为比较玄学,要先去路由器做好配置,然后在阿里云上做好配置,要保证一个顺序的先后性。

    2.打通外部到公司内部的VPN通道

    也就是下图中的移动办公的这种场景:


    移动办公VPN

    要达到的效果说的实例化一点就是

    ping 188.100.100.1
    

    能够ping通,值得注意的一点是这里的188.100.100.1是阿里云上服务器的内网ip,也就是说,并不只是从公司外访问公司内的某台设备那样简单,而是要穿过两层vpn隧道。

    这里要在网关上分配一个网段,来给进来的设备。相当于从外部访问的设备进来后,就相当于一个内网的设备。

    采用的是PPTP协议连接。在路由器上做些配置就OK了。


    在路由器的networks中设置

    Windows电脑自带PPTP,Mac电脑可以用Shimo软件连接。填写公司的ip地址,和分配给你的username和password就好了。

    这样后,可以ping通内网设备,但是并不能ping的通188.100.100.1。这个问题很奇葩。查了很多资料,终于找到了这个办法试了试。

    v2ex上的v友的回答

    但这里值得注意的一点是:在一些路由器上,如unifi路由器,在输入框中是不允许填写和内网网段重复的网段的。所以,我的做法是到路由器中,改写etc/pptpd.conf这个文件,设置remoteip,把它的网段改为和内网相同的网段。这里还有一点是值得注意的是,分配的ip不要和已有的分配给内网的设备的ip重复。所以我的做法是选择了ip网段的后面几位。localip是pptp的服务器的地址。如果用traceroute命令查看,访问非局域网,会有显示这个地址。
    更改后,保存,并使其生效。重启生效的命令为:

    sudo service pptpd restart
    

    3.精华:解决动态IP导致的问题

    如今,无论是对于家庭网络还是公司网络(家庭网络与公司网络一个重要的区别是:公司网络是上传和下载的速度相同),网络运营商默认提供的网络,都不是固定IP,而是动态IP。如果我们去找网络运营商去要求固定IP,运营商会很客服的告诉你,当然可以提供这项套餐的,然后转而告诉你,这个得加钱。而固定套餐的价格相比于动态IP的套餐的价格要高出数倍。


    因为遇到了动态IP,使得我原有的设置,复杂了至少两倍。我所遇到的困难:

    • IP地址经常更换,每次重启路由器后,都会导致IP变化。这使得如果IP每次变化后,我都需要到阿里云上重新新建用户网关和IPsec连接,同时配置本地网络。还有pptpd.conf文件也会重新恢复原样。

    • Unifi网关的路由器系统的Linux版本,对linux包源进行了限制,比如wget都不能下载安装,所幸提供了Python,但是也仅仅支持Python2,Python的第三方包也不能下载,比如常用的requests包。

    我们把这些问题拆分化,一一解决:

    通过阿里云API接口来创建IPsec连接

    对于创建用户网关和IPsec连接,阿里云是提供一个公共的API接口用户创建和删除的,可以参考这个链接阿里云VPN接口文档

    当然并不只是看文档这么简单,文档里并没有提供Python的示例代码。搜索后发现一些都是Java代码,零星的几个Python代码还跑不起来。所有这里直接黏贴上我组合后的Python2代码,Python3只要稍稍改动部分,就可以跑的起来,这个我稍后会写。
    Python2调用阿里云API示例代码:

    # -*- coding: utf-8 -*-
    import base64
    import urllib
    import hmac
    from hashlib import sha1
    import requests
    import uuid
    import time
    import hmac,ssl
    ALIYUN_ACCESS_KEY_ID=""#在阿里云上生成
    ALIYUN_ACCESS_KEY_SECRET=""#在阿里云上生成
     
    try:
        _create_unverified_https_context = ssl._create_unverified_context
    except AttributeError:
        # Legacy Python that doesn't verify HTTPS certificates by default
        pass
    else:
        # Handle target environment that doesn't support HTTPS verification
        ssl._create_default_https_context = _create_unverified_https_context
     
    D = {
        'Format':'JSON',
        'Version':'2016-04-28',
        'SignatureMethod':'HMAC-SHA1'
        }
    timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
    D['SignatureNonce']=str(uuid.uuid1())
    D['SignatureVersion']=1.0
    D['AccessKeyId']=ALIYUN_ACCESS_KEY_ID
    D['Timestamp']=timestamp
     
     
    def percent_encode(encodeStr):
        encodeStr = str(encodeStr)
        #res = urllib.parse.quote(encodeStr)
        res = urllib.quote_plus(encodeStr)
        res = res.replace('+', '%20')
        res = res.replace('*', '%2A')
        res = res.replace('%7E', '~')
        res = res.replace('/', '%2F')  #That is for the LocalSubnet "/"
        return res
     
    def sign(parameters):
        sortedParameters = sorted(parameters.items(), key=lambda parameters: parameters[0])
        canonicalizedQueryString = ''
        for (k, v) in sortedParameters:
            canonicalizedQueryString += '&' + percent_encode(k) + '=' + percent_encode(v)
        print(canonicalizedQueryString)
        stringToSign = 'GET&%2F&' + percent_encode(canonicalizedQueryString[1:])  
        bs = ALIYUN_ACCESS_KEY_SECRET + '&'
        bs = bytes(bs).encode('utf8')
        stringToSign = bytes(stringToSign).encode('utf8')
        h = hmac.new(bs, stringToSign, sha1)
        # 进行编码
        signature = base64.b64encode(h.digest()).strip()
        return signature
    
    def genipsec(ip,customId):
        D['Action']="CreateVpnConnection"
        D['CustomerGatewayId']=customId
        D['VpnGatewayId']=""
        D['RegionId']="cn-shanghai"
        D['Name']="newIPsec"+ip
        D['LocalSubnet']=""
        D['RemoteSubnet']=""
        D['EffectImmediately']=True
        D['IkeConfig']={"Psk":"",'IkeVersion':'','IkeMode':'','IkeEncAlg':'',
                'IkeAuthAlg':'','IkePfs':'','IkeLifetime':'','LocalIdIPsec':'','RemoteId':ip}
        D['IpsecConfig']={'IpsecEncAlg':'','IpsecAuthAlg':'','IpsecPfs':'','IpsecLifetime':''}
        D['Signature'] = sign(D)
         
         
        sortedParameters = sorted(D.items(), key=lambda D: D[0])
         
        url = 'https://vpc.aliyuncs.com/?' + urllib.urlencode(sortedParameters)
        r = requests.get(url)
        print(r.text)
    
    • 关于这块代码的原理可以参见这篇文章阿里云 API 签名机制的 Python 实现

    • 将此段Python2的代码变为python3的代码,需要改动两个地方,一个是import urllib变为import urllib.parse,还有就是就是转码python2为bytes(bs).encode('utf8'),python3为bytes(bs,encoding='utf8')

    • 阿里云提供的文档中,有一个容易引起人们误解的地方就是:

      阿里云vpn文档
      我在代码中要构建一个字典,需要构建key和value,这里的key值,不要填写阿里云文档中的IkeConfig.Psk这样的格式,而是添加PSK,如下:
    D['IkeConfig']={"Psk":"",'IkeVersion':'','IkeMode':'','IkeEncAlg':'',
                'IkeAuthAlg':'','IkePfs':'','IkeLifetime':'','LocalIdIPsec':'','RemoteId':ip}
    
    • 其他示例的Python代码中还有一种情况没有涉及,就是因为我需要在LocalSubnet那填写网段,这个网段里包含“/”字符,所以需要处理“/”这个字符的转换,所以我添加了如下的代码。
     res = res.replace('/', '%2F')  #That is for the LocalSubnet "/"
    
    如何监测IP变化

    方案是这样:在路由器的linux系统中通过crontab跑一个定时检测ip变化的脚本,如果有变化则触发相应的后续的一系列创建和修改的操作。所以包含四部分:
    1.crontab怎么写?
    2.如何获取实时公网ip?
    3.在哪找到源ip来实时的与当前ip做对比来判断ip是否发生了变化?
    4.在一个处处受限的linux系统下,如何进行一系列的后续复杂操作?

    • crontab 定时执行python脚本

    关于crontab的用法可以参考这篇文章crontab 定时执行python脚本。通过crontab命令的一个重大好处是:机器重启之后,依然生效。
    用到的主要是这两条命令:

    crontab -e   #编写定时任务
    crontab -l    #列出已经在运行的定时任务
    

    每两分钟检查一次



    在当前目录下,生成了log


    • 获取实时的公网IP的方法

    可以用如下代码:

    #!/usr/bin/python
    # -*- coding:utf8 -*-
    
    import urllib2
    import re
    
    url = urllib2.urlopen("http://txt.go.sohu.com/ip/soip")
    text = url.read()
    ip = re.findall(r'\d+.\d+.\d+.\d+',text)
    
    print(ip[0])
    

    转换为python3的代码需要改动urllib.parse

    • 判断公网ip是否变化

    这个解决方法,并不能把第一次的ip给写死,因为路由器会经常重启,不能每次重启都要重新更改代码,把ip重新写死。这里的做法是在路由器中读取etc/ipsec.conf的文件,这个文件中有当前路由的公网ip,而且当ip变化时,我们也需要更改这个文件。

    def getOriginIp():
        with open('/etc/ipsec.conf', 'r+') as f:
            for line in f.readlines():
                if re.search('left=',line):
                    text=line.decode('GBK')
                    ip = re.findall(r'\d+.\d+.\d+.\d+',text)
                    return ip[0]
    

    通过这个OriginIp与当前的IP进行对比,如果有变化,需要去执行后面的一系列操作。

    • 在一个处处受限的linux系统下,如何进行一系列的后续复杂操作?

    Unifi路由器的linux系统处处受限,那后续的操作我就不放在路由器上了。于是,我就把后续的一些复杂点的操作移到了另外一台服务器上,路由器上的任务就变成了只负责检测ip变化,如果有变化,通过curl命令去触发另一台服务器。

    在另一台服务器上,搭建一台web服务器,我的愿景是尽量少下载第三方包,所以我并没有flask框架,而是用了python原生的一个web服务。大家可能之前听说python中有个内建的http服务器,只要通过这个命令就能开启(详情参见非常简单的PYTHON HTTP服务):

     python -m SimpleHTTPServer 8080
    

    于是,我就找到了Python2自带的包BaseHTTPRequestHandler,用法可以参考这篇文章Python BaseHTTPServer 介绍。我这里展示一个最简单的用法,通过crul http://localhost:8888就可以执行HelloWorld函数。

    from BaseHTTPServer import BaseHTTPRequestHandler
    import cgi
    import json
    
    
    def HelloWorld():
        print("hello world")
    
    
    class TodoHandler(BaseHTTPRequestHandler):
        """A simple TODO server
    
        which can display and manage todos for you.
        """
    
        # Global instance to store todos. You should use a database in reality.
        TODOS = []
    
        def do_GET(self):
            # return all todos
    
            if self.path != '/':
                self.send_error(404, "File not found.")
                return
    
            # Just dump data to json, and return it
            message = json.dumps(self.TODOS)
           HelloWorld()
    
            self.send_response(200)
            self.send_header('Content-type', 'application/json')
            self.end_headers()
            self.wfile.write(message)
    
        def do_POST(self):
            """Add a new todo
    
            Only json data is supported, otherwise send a 415 response back.
            Append new todo to class variable, and it will be displayed
            in following get request
            """
            ctype, pdict = cgi.parse_header(self.headers['content-type'])
            if ctype == 'application/json':
                length = int(self.headers['content-length'])
                post_values = json.loads(self.rfile.read(length))
                self.TODOS.append(post_values)
            else:
                self.send_error(415, "Only json data is supported.")
                return
    
            self.send_response(200)
            self.send_header('Content-type', 'application/json')
            self.end_headers()
    
            self.wfile.write(post_values)
    
    if __name__ == '__main__':
        # Start a simple server, and loop forever
        from BaseHTTPServer import HTTPServer
        server = HTTPServer(('localhost', 8888), TodoHandler)
        print("Starting server, use <Ctrl-C> to stop")
        server.serve_forever()
    

    所以可以把一些后续操作,移动到另一台服务器上来。

    动态DNS(DDNS)来解决动态IP

    搭建外部访问公司的vpn通道是使用的pptp协议,因为我们的IP是动态IP,经常更换,所以一个比较好的解决办法是,把ip都解析到同一个域名下。这就引出了DDNS的概念。Unifi路由器配置的方法参见这个教程UniFi - 动态 DNS 配置
    还有这个教程在usg里怎么填写动态DNS?。其中freedns是一个提供免费DDNS的网站,可以提供给你一个免费的域名来做DDNS的解析。在freedns注册登陆后,并选择好域名后,进入Dynamic DNS页面移动到底,会看到你的域名和ip地址。如果我想更改ip,可以点击quick

    freedns
    如果我想更改ip,可以点击quick,进入到另一页面,其中提供的方法就是用wget把ip推送给freedns网站,来达到更改ip的效果。
    命令行更改ip

    所以每当检测到更换ip后,我们通过代码:

    os.system('wget -O http://freedns的地址')
    

    来更换域名和ip的绑定关系。

    结尾

    “有钱能使鬼推磨”,这是一句无比正确却毫无价值的一句话。直接多花些钱,当然可以升级成固定IP。但如果切换到上帝视角,就会发现,动态IP是很多企业和家庭所共同面临的一个问题。只是可能我平时上网需求,动态IP对我的上网没有任何影响。但如果当我们准备对网络搞些事情的时候,动态IP的问题就会铺面而来。所以我想如果不回避动态IP,而是想出一个解决办法,实际上是找出了一个对全行业都有价值的通用性解决方案。

    参考链接:
    极客时间| VPN:朝中有人好做官
    v2ex|用 vpn 连上公司内网后无法访问内网其他服务器
    阿里云VPN接口文档
    阿里云 API 签名机制的 Python 实现
    crontab 定时执行python脚本
    非常简单的PYTHON HTTP服务
    Python BaseHTTPServer 介绍
    UniFi - 动态 DNS 配置
    在usg里怎么填写动态DNS?

    相关文章

      网友评论

          本文标题:如何解决动态IP导致的VPN失效

          本文链接:https://www.haomeiwen.com/subject/jnmfvftx.html