Pages

Monday, 26 March 2018

树莓派利用nginx反向代理,成为IPv6/IPv4公网服务器(需要远程双栈服务器)

效果

树莓派只需连接电源,以及有免费IPv6服务的网线,即可成为公网上的IPv6服务器;如果有同时支持IPv4和IPv6的双栈服务器,树莓派还能成为公网上的IPv4服务器。

动机

之前发现学校提供免费不间断的IPv6服务,而且DigitalOcean的VPS也能提供IPv6网络,所以萌生了自己建立服务器的想法。
鉴于需要一台耗电量小、噪声小且能够长期不关机运行的服务器,自然而然想到了树莓派。于是经过几天的折腾,终于实现了把树莓派作为公网服务器的想法,但离理想情况还是有些差距,主要特点:
  • 本地而言只需要用到IPv6网络,对IPv4网络没有要求
  • 作为IPv6服务器,采用DDNS方法,但向DNSPod更新DNS信息需要有本地或远程IPv4网络的支持
  • 作为IPv4服务器,采用nginx反向代理,一定需要远程双栈服务器(理论上本地双栈电脑也可以)

大体思路

这里假设的都是采用远程服务器,即DigitalOcean在旧金山的同时支持IPv4和IPv6的VPS。

作为IPv6服务器

此时采用DDNS方法,因为自己有域名,而DNSPod支持IPv6的AAAA记录,且DNSPod提供API,所以采用每次IPv6地址变化则向DNSPod发送修改记录请求,这样实现DDNS。
其中存在的问题是DNSPod并不支持纯IPv6环境下的API请求,即发送请求必须用到IPv4网络(通过IPv4网络发送IPV6记录的修改请求)。所以采用远程服务器,本地通过IPv6作为http的client访问远程服务器,远程服务器作为http server接收请求,并记录请求的IPv6地址(即本地服务器IPv6地址),然后远程服务器通过IPv4调用DNSPod API,修改对应记录,实现DDNS。

作为IPv4服务器

此时采用反向代理方法,远程服务器nginx设置反向代理,地址为本地服务器IPv6地址。这样可以利用DDNS时远程服务器得到的IPv6地址,来更新nginx中反向代理的设置。
举例来说,当某个IPv4用户想要访问本地服务器时,其通过域名访问首先会通过IPv4访问到远程服务器,远程服务器反向代理将请求通过IPv6发送到本地服务器,本地服务器作出响应后,将结果通过IPv6返回到远程服务器,远程服务器最后通过IPv4返回到用户。
可以说一次请求需要跨越4次太平洋,但根据实际测试,响应时间还是可以接受的。

具体实现

关于树莓派

树莓派并没有使用官方固件,而是最初采用了Ubuntu 14.04 LTS,其后转移到Ubuntu 15.04。因为是作为server用,所以更倾向于采用Ubuntu的server版本。
关于树莓派如何连上IPv6网络,实际就是Ubuntu如何连上IPv6网络了,这样的例子很多,对于某些特殊情况,我之前也有写可能的解决方法。

远程服务器端

因为本地IPv6地址是DHCP分配,一直在变动,所以可能需要频繁向远程服务器发送请求。因为我的远程服务器运行有Flask应用,而我对Python比较熟悉,加上对于方案实现我比较倾向于用http实现,所以考虑再三决定采用nginx+uwsgi+flask+python
结构:nginx设定请求的URL,将此URL的请求设定为转发到flask应用上;uwsgi作为flask和nginx之间的桥梁,并且用来管理flask应用;flask作为网络框架提供网络支撑,处理请求数据和数据的返回;python作为flask里的具体实现,用来调用DNSPod API,以及修改nginx配置文件,实现动态的反向代理。

DDNS部分

先贴主要代码:
def update_dnspod(self, ip):
    url = 'https://dnsapi.cn/Record.Modify'
    info = {
        'login_email': 'xxx',
        'login_password': 'xxx',
        'format': 'json',
        'domain_id': '23606146',
        'record_id': '116841773',
        'record_line': '默认',
        'sub_domain': 'test',
        'record_type': 'AAAA',
        'value': ip
    }

    data = urllib.parse.urlencode(info).encode()
    t = urllib.request.urlopen(url, data=data)
    return t.read().decode('utf-8')
实际上就是调用API了,我这里是在更新test.mydomain.com的记录。
关于需要domain_id和record_id,似乎只能从API获取,写了一段代码用来获取:
import urllib.request
import urllib.parse
import json

login_email = 'THE_EMAIL'
login_password = 'THE_PASSWORD'


def get_json(url, data):
    con = urllib.request.urlopen(
        url, data=urllib.parse.urlencode(data).encode())
    return json.loads(con.read().decode())


def get_domain_id():
    url = 'https://dnsapi.cn/Domain.List'
    data = {
        'login_email': login_email,
        'login_password': login_password,
        'format': 'json'
    }
    json = get_json(url, data)
    for domain in json['domains']:
        print('domain name:%s\tdomain id:%s' % (domain['name'], domain['id']))


def get_record_id(domain_id):
    url = 'https://dnsapi.cn/Record.List'
    data = {
        'login_email': login_email,
        'login_password': login_password,
        'format': 'json',
        'domain_id': domain_id
    }
    json = get_json(url, data)
    print('domain name:%s\tdomain id:%s' %
          (json['domain']['name'], json['domain']['id']))
    for record in json['records']:
        print('record name:%s\trecord type:%s\trecord id:%s' %
              (record['name'], record['type'], record['id']))


if __name__ == '__main__':
    get_domain_id()
注意:
对于修改记录API的调用,DNSPod有限制:如果1小时之内,提交了超过5次没有任何变动的记录修改请求,该记录会被系统锁定1小时,不允许再次修改。比如原记录值已经是 1.1.1.1,新的请求还要求修改为 1.1.1.1。

反向代理部分

nginx配置文件中反向代理server:
server {
    listen 80;
    server_name pi.mydomain.com;

    location / {
        proxy_pass http://[2001:250:5002:8100::3:9b63]; # for raspberry pi
        proxy_set_header Host $host;
    }
}
python实现代码:
def update_nginx_config(self, ip):
    with open('/etc/nginx/sites-available/default', 'r') as f:
        s = f.read()
    r = re.compile(r'http://\[(.*?)\]; \# for raspberry pi')
    s = s.replace(r.findall(s)[0], ip)
    with open('/etc/nginx/sites-available/default', 'w') as f:
        f.write(s)
    os.popen('service nginx restart')
读取配置文件后,通过正则表达式找到需要修改的IPv6地址(所以# for raspberry pi必不可少),再通过字符串替换(尝试直接用正则表达式替换,但一直没实现),重新写入配置文件;最后重启nginx服务。
实际上这是一段很危险的代码,会直接修改nginx配置文件,并且通过命令行重启nginx服务。

Flask配置

先贴代码:
from flask import Flask, request
import Ddns
application = Flask(__name__)


@application.route('/ipv6')
def ipv6():
    code = request.args['code']
    if code != '80':
        return 'Bad Request'
    ip = request.remote_addr
    d = Ddns.Ddns()
    t = d.update(ip)
    return t

if __name__ == '__main__':
    application.debug = True
    application.run()
客户端发送GET请求,Flask收到请求后首先校对code,用来防止恶意请求。然后获取客户端IP地址,交给python去实现。

uwsgi配置

关于nginx+uwsgi+flask的配置在之前的文章中介绍过,这里就只写主要部分

systemd中配置服务

路径:/etc/systemd/system/ddns.service
[Unit]
Description=uWSGI instance to serve DDNS
After=network.target

[Service]
User=root
Group=root
WorkingDirectory=/home/zhantong/ddns
Enrironment="PATH=/usr/bin"
ExecStart=/usr/bin/uwsgi --ini ddns.ini

[Install]
WantedBy=multi-user.target
这里设定User和Group均为root是极其危险的,但也是必须的,因为需要读取和写入nginx配置文件,并且通过命令行重启nginx服务。

uwsgi配置文件

路径:/home/zhantong/ddns
[uwsgi]
module = proxy_and_ddns

processes = 1
vhost = true
socket = ddns.sock
chmod-socket = 777
vacuum = true
die-on-term = true
这里其实有一些细节问题,就不说了。
proxy_and_ddns就是proxy_and_dds.py,flask代码。

nginx配置

最后需要加入请求URL,并将其转发到flask,所以nginx关于这部分的server:
server {
    listen [::]:80 ipv6only=on;
    server_name ipv6.mydomain.com;

    location / {
        include uwsgi_params;
        uwsgi_pass unix:/home/zhantong/ddns/ddns.sock;
    }
}
ipv6.mydomain.com在DNSPod中设置了记录为AAAA,指向远程服务器的IPv6地址。

本地服务器

本地服务器的工作其实很简单,就是对对应URL发送GET请求
import urllib.request

code = '80'
urllib.request.urlopen('http://ipv6.mydomain.com/ipv6?code=' + code)
可以利用Ubuntu中的corn来实现定时运行。

结束

上面说的很乱,但理顺了还是挺容易的,最后的结果就是test.mydomain.com对应的IP就是树莓派的IPv6地址,即通过IPv6访问test.mydomain.com就是直接访问树莓派;而pi.mydomain.com是通过远程服务器反向代理,通过IPv4访问树莓派。

附录

flask和python完整代码

flask

文件名:proxy_and_ddns.py
from flask import Flask, request
import Ddns
application = Flask(__name__)


@application.route('/ipv6')
def ipv6():
    code = request.args['code']
    if code != '80':
        return 'Bad Request'
    ip = request.remote_addr
    d = Ddns.Ddns()
    t = d.update(ip)
    return t

if __name__ == '__main__':
    application.debug = True
    application.run()

python

文件名:Ddns.py
import urllib.request
import os
import re


class Ddns():

    def update_nginx_config(self, ip):
        with open('/etc/nginx/sites-available/default', 'r') as f:
            s = f.read()
        r = re.compile(r'http://\[(.*?)\]; \# for raspberry pi')
        s = s.replace(r.findall(s)[0], ip)
        with open('/etc/nginx/sites-available/default', 'w') as f:
            f.write(s)
        os.popen('service nginx restart')

    def update_dnspod(self, ip):
        url = 'https://dnsapi.cn/Record.Modify'
        info = {
            'login_email': 'xxx',
            'login_password': 'xxx',
            'format': 'json',
            'domain_id': '23606146',
            'record_id': '116841773',
            'record_line': '默认',
            'sub_domain': 'test',
            'record_type': 'AAAA',
            'value': ip
        }

        data = urllib.parse.urlencode(info).encode()
        t = urllib.request.urlopen(url, data=data)
        return t.read().decode('utf-8')

    def update(self, ip):
        t=self.update_dnspod(ip)
        self.update_nginx_config(ip)
        return t

参考

No comments:

Post a Comment