阿里云DDNS使用方法
说明:任何ddns都很难保证100%SLA,所以我还使用贝锐的花生壳作为辅助线路。
总体方案:
- 部署Python环境
- 修改阿里云DDNS的Python脚本
- 设置可执行脚本
- 让服务器定时启动脚本,完成ddns的更新与检查
需要依赖的外部服务:
- 3322提供的查询公网ipv4的接口
- 阿里云域名解析服务
- 新网上的域名
需要资费:
- 域名的租金
总体流程:
- 借助conda封装好一个便携版Python执行环境
- 用这个便携版Python去执行ddns代码
- 设置定时执行
这样的好处是不污染服务器的Python环境,同时不会因为系统Python环境崩溃而受到影响。
什么时候使用DDNS?
一句话:只有在你的服务器被分配动态ipv4公网或私网ip的时候才考虑使用DDNS。
为什么会是动态?怎么出现这种情况的?
据估算,大陆一共有2-4亿个公网ip1,显然不够用,其中中国电信的公网ip最多2。
很显然ip不够用,于是运营商采取NAT技术,打个比方,一个小区或者一个信号塔使用公网ip,小区内或信号塔范围内的设备在局域网中上网的方式。这样就大大节约公网ip资源。
但是你在家里搭建的网址、NAS等服务别人访问不到的。电信和联通有时在ip宽裕的情况下会就近给一些宽带用户账号分配公网ipv4,且每天变化一次。所以北上广深的宽带用户大概率是有动态ip的,没有就打电话。
免费获得动态公网ip的唯一方式:
购买电信或联通宽带,价格在1000元以上/年的百兆以上宽带,因为高速宽带为了保证稳定性,运营商会给你的宽带直接使用动态公网ip,这样省去你做第2步。
拨打电信10000或联通10010客服,让客服和技术师傅给你的宽带转成动态公网ip,这个请求合理合法,如果对方拒绝,可向工信部投诉。此时你的光猫分配了公网ip,但是这个ip每天一变。(固定的公网ip只有企业和政府单位才能申请,且价格至少25000元一年,根据网速上不封顶。)
再次拨打10000或10010,询问自己宽带的账号和密码。
登录到光猫后台网址,一般是192.168.1.1 ,将网络连接方式从路由模式改成桥接模式。这样做是让光猫的动态公网ipv4直接交给路由器。设置完后你的光猫就不负责宽带账号登录了,它只负责网络传输。(踩坑:刚开始电信给我装的光猫是SDN,这种光猫是进不了后台的,家里是这种光猫的赶快淘宝上买了一个华为的光猫换上,这样就能进后台改桥接了。)
用网线将路由器接到光猫上,此时路由器将获得动态公网ip(因为桥接),进入路由器后台网站,将你的宽带账号和密码填进去,并登录。即采用路由器拨号上网。
此处的原文链接:https://blog.csdn.net/qq_43626147/article/details/114196718
购买域名并获取access key
去阿里云买一个域名。这里没啥说的。
然后获取 access key ,用于使用Python脚本自动更新域名的dns配置。
鼠标移到阿里云账号头像上,点击AccessKey,点击获取即可,最好下载csv保存,防止忘了。csv文件是可以用office或wps打开的。
阿里云会给出提示,建议不要用root access key,而使用RAM用户管理,这里不赘述,参考阿里云的配置方法搞好即可。总之最后需要得到能用来更新域名信息的access key即可。

部署Python环境
需要在服务器上有一个Python环境。
为了避免环境互相的污染,建议使用conda封装好一个专用的环境部署在服务器上,如果服务器不想安装conda,那么在一个有conda的电脑上搞好,然后把env的文件夹复制过去,通过脚本的方式启动这个Python环境即可,甚至服务器都不需要搞Python环境。
做法:
首先创建一个全新的conda环境,为了迁移做准备。
我的服务器是Windows server,我的计算机是Windows pro,我的个人计算机上有codna环境。
然后安装库:我新建了一个环境,然后在此环境下安装必要的库,我们需要的是下面的库,使用pip安装。
pip install aliyun-python-sdk-alidns
以及json,urllib库,这两个库应该已经自带了。
此时,我们实际上是得到了一个“绿色版”的Python解释器,可以把这个Python所在的文件夹复制到服务器上,直接运行,来实现ddns服务。
然后需要把这个搞好环境的Python的运行文件夹复制到ddns的文件夹下面。去conda安装环境的路径下面找到你创建的这个环境,把文件夹复制过来就行。
创建Python脚本用于ddns
创建以下脚本,并命名为ddns.py。
需要按照注释将修改的地方修改了。
#!/usr/bin/env python3
# coding=utf-8
import json
import urllib
from aliyunsdkalidns.request.v20150109 import DescribeSubDomainRecordsRequest, AddDomainRecordRequest, UpdateDomainRecordRequest, DeleteDomainRecordRequest
from aliyunsdkcore.acs_exception.exceptions import ServerException
from aliyunsdkcore.acs_exception.exceptions import ClientException
from aliyunsdkcore.client import AcsClient
################################### 配置段:开始 ################################################
# AccessKey 和 Secret 建议使用 RAM 子账户的 KEY 和 SECRET 增加安全性
ID = 你的接入访问控制id
SECRET = 你的接入访问控制key
# 地区节点 可选地区取决于你的阿里云帐号等级,普通用户只有四个,分别是杭州、上海、深圳、河北,具体参考官网API
regionId = 'cn-hangzhou'
# 配置认证信息
client = AcsClient(ID, SECRET, regionId)
# 设置主域名
DomainName = 'limitless.net.cn' # 此处是你需要配置的接入域名
# 子域名列表 列表参数可根据实际需求增加或减少值 “@” 默认域名, “www”即“www.你的域名”,以此类推。 “*” 是通配符,代表所有子域名
SubDomainList = ["mc", '@']
# 获取外网IP 三个地址返回的ip地址格式各不相同,3322 的是最纯净的格式
ip_api = 'http://www.3322.org/dyndns/getip'
# 用于在记录相同的时候不更新域名记录
exit_when_ok = True
################################### 配置段:结束 ################################################
################################### 函数定义段:开始 ######################################
def getIp():
# 备选地址: 1, http://pv.sohu.com/cityjson?ie=utf-8 2,curl -L tool.lu/ip
with urllib.request.urlopen(ip_api) as response:
html = response.read()
ip = str(html, encoding='utf-8').replace("\n", "")
print(f"通过 {ip_api} 得到的 ipv4 地址为\n ip = '{ip}' ")
return ip
def getDomainInfo(SubDomain):
request = DescribeSubDomainRecordsRequest.DescribeSubDomainRecordsRequest()
request.set_accept_format('json')
# 设置要查询的记录类型为 A记录 官网支持A / CNAME / MX / AAAA / TXT / NS / SRV / CAA / URL隐性(显性)转发 如果有需要可将该值配置为参数传入
request.set_Type("A")
# 指定查记的域名 格式为 'test.example.com'
request.set_SubDomain(SubDomain)
response = client.do_action_with_exception(request)
response = str(response, encoding='utf-8')
# 将获取到的记录转换成json对象并返回
return json.loads(response)
# 新增记录 (默认都设置为A记录,通过配置set_Type可设置为其他记录)
def addDomainRecord(client, value, rr, domainname):
request = AddDomainRecordRequest.AddDomainRecordRequest()
request.set_accept_format('json')
# request.set_Priority('1') # MX 记录时的必选参数
request.set_TTL('600') # 可选值的范围取决于你的阿里云账户等级,免费版为 600 - 86400 单位为秒
request.set_Value(value) # 新增的 ip 地址
request.set_Type('A') # 记录类型
request.set_RR(rr) # 子域名名称
request.set_DomainName(domainname) # 主域名
# 获取记录信息,返回信息中包含 TotalCount 字段,表示获取到的记录条数 0 表示没有记录, 其他数字为多少表示有多少条相同记录,正常有记录的值应该为1,如果值大于1则应该检查是不是重复添加了相同的记录
response = client.do_action_with_exception(request)
response = str(response, encoding='utf-8')
relsult = json.loads(response)
return relsult
# 更新记录
def updateDomainRecord(client, value, rr, record_id):
request = UpdateDomainRecordRequest.UpdateDomainRecordRequest()
request.set_accept_format('json')
# request.set_Priority('1')
request.set_TTL('600')
request.set_Value(value) # 新的ip地址
request.set_Type('A')
request.set_RR(rr)
request.set_RecordId(record_id) # 更新记录需要指定 record_id ,该字段为记录的唯一标识,可以在获取方法的返回信息中得到该字段的值
response = client.do_action_with_exception(request)
response = str(response, encoding='utf-8')
return response
# 删除记录
def delDomainRecord(client, subdomain):
info = getDomainInfo(subdomain)
if info['TotalCount'] == 0:
print('[Error] 尝试删除记录但是没有相关的记录信息,删除失败!')
elif info["TotalCount"] == 1:
request = DeleteDomainRecordRequest.DeleteDomainRecordRequest()
request.set_accept_format('json')
record_id = info["DomainRecords"]["Record"][0]["RecordId"]
request.set_RecordId(record_id) # 删除记录需要指定 record_id ,该字段为记录的唯一标识,可以在获取方法的返回信息中得到该字段的值
result = client.do_action_with_exception(request)
print('删除记录成功,返回:')
print(result)
else:
# 正常不应该有多条相同的记录,如果存在这种情况,应该手动去网站检查核实是否有操作失误
print("[Error] 尝试删除记录时,存在多个相同子域名解析记录值,请控制台核查后再操作!")
# 有记录则更新,没有记录则新增
def setDomainRecord(client, value, rr, domainname):
info = getDomainInfo(rr + '.' + domainname)
if info['TotalCount'] == 0:
print('尝试添加记录...')
add_result = addDomainRecord(client, value, rr, domainname)
print(add_result)
elif info["TotalCount"] == 1:
print('尝试更新已有...')
record_id = info["DomainRecords"]["Record"][0]["RecordId"]
cur_ip = IP
old_ip = info["DomainRecords"]["Record"][0]["Value"]
if cur_ip == old_ip:
print("相同的指向,无需更新。")
if exit_when_ok:
print("当前设置域名正常时直接停止轮询其他域名,程序退出。")
exit(0)
else:
update_result = updateDomainRecord(client, value, rr, record_id)
print('更新成功,返回:')
print(update_result)
else:
# 正常不应该有多条相同的记录,如果存在这种情况,应该手动去网站检查核实是否有操作失误
print("[Error] 存在多个相同子域名解析记录值,请控制台核查删除后再操作!")
################################### 函数定义段:结束 ######################################
################################### 程序入口运行处:开始 ########################################
# 查询记录
IP = getIp()
# 循环子域名列表进行批量操作
i = 1
for x in SubDomainList:
print(f"$ {i:2d}. 对于'{x}'进行操作...")
setDomainRecord(client, IP, x, DomainName)
i = i+1
# 删除记录测试
# delDomainRecord(client,'b.jsoner.com')
# 新增或更新记录测试
# setDomainRecord(client,'192.168.3.222','a',DomainName)
# 获取记录测试
# print (getDomainInfo(DomainName, 'y'))
# 批量获取记录测试
# for x in SubDomainList:
# print (getDomainInfo(DomainName, x))
# 获取外网ip地址测试
# print ('(' + getIp() + ')')
exit(0)
创建启动脚本
再创建一个bat脚本文件。
此时将conda创建好的可执行当前Python环境的这个env复制出来,保存在ddns.py所在的目录中,结构如同:
ddns-aliyun
|----目录:Python env 的目录,这是conda创建的Python解释器的目录与环境内容
|----文件:ddns.py
|----文件:ddns.bat
编辑bat脚本文件(我的系统是Windows,Linux需要创建sh文件)内容为:
绝对路径到目录:Python env 的目录\python.exe 绝对路径到目录\ddns-aliyun\DDNS.py
Linux参考这个写法,总之,实现一个shell脚本,从创建的那个conda环境的Python来启动DDNS.py就行。
这里看起来有点复杂,实际上不复杂。
我们的目的是,在不给服务器安装conda的情况下,实现一个不影响系统Python环境的独立的Python环境,用于ddns服务。这样系统Python环境崩溃了也不会影响我们的ddns服务。
其实就是搞了个“绿色便携”版本的Python,作为一个文件夹整体,复制粘贴到服务器上,然后运行这个“绿色”版Python去执行我们的ddns脚本,实现ddns更新。
只不过这里使用了conda,为了方便。其实也可以搞个绿色版Python封装一遍是一样的。
到目前为止,我好像遇见了问题?
如果你没看明白上面的操作,那么理解下面的文字:
目的:得到一个独立的Python环境,用这个Python环境定时执行我们的ddns.py脚本,去实现ddns服务。
然后用你擅长的方式实现就行了。不一定局限于conda或者Python便携版什么的,或者如果你不在乎环境污染的问题,那你直接一锤子梭哈,直接用系统环境的Python去执行也没啥问题(就是系统环境如果更新或者更改的话有可能影响ddns业务的稳定性)
如何定时执行这个bat脚本?
八仙过海,各显神通!
总之只要能定时执行就行。我用的宝塔面板的计划任务实现的,你也可以研究一下怎么使用Windows server的计划任务之类的方法。
或者再写个Python脚本去计划执行那个bat,然后给这个脚本写个看门狗,再给看门狗写个看门狗……..
阿里云免费版的dns生效时间(TTL)是10分钟,而检查ip是否变化也不能太频繁(3322的api调用太频繁会被暂时封禁一会)。
我设置的是7分钟运行一次ddns.py。最差情况下,可能需要17分钟,dns配置才会恢复。设置的太短了容易被封禁,设置太久需要等待更长的时间。
不过事实上7分钟够用了,平均感觉5分钟就能完成dns更新,可以正常访问。
所以,建议设置为7分钟定时执行。
2024年6月2日
