效果
树莓派只需连接电源,以及有免费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.pyfrom 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.pyimport 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