基于python利用腾讯云API Explorer实现家用宽带的动态DNS解析

摘要

先介绍了应用背景:用来使得域名能够正确对应上动态IP。然后介绍了完成这项功能需要用到的API接口,并简单实现了对应的API接口调用框架。最后为了使用更加简洁,对程序逻辑作了进一步优化。实践证明真理就在实践中!

引言

总所周知,一般的家用宽带是很多条线路共用一个出口IP的。但是对与电信宽带来说,由于电信IP比较多,因此通过安装家用摄像头的方式借口来申请公网IP。但是为了防止用户使用ip在家里搭建不可描述的服务,电信给的公网IP一般都是动态IP,而且封掉了80,443,8080端口。根据笔者的观察来看,宽带的出口IP每三天换一次,即72小时换一次。同时,如果在三天内家里的光猫重启了,或者遭遇断电再来电的事故,IP地址也会变化。因此在使用家用宽带提供外网服务时,首先需要解决的是不断变化的IP地址的问题。

目前市面上已经存在很多的动态域名解析服务(DDNS),即根据你当前的IP地址,实时修改对应域名的在公共名字服务器上的A记录,使得用户在访问你的域名时能够正确达到你的服务器地址。比较有名气的有:

  1. 阿里DDNS(和本文原理一样的)
  2. 花生壳(内网穿透,通过第三方服务器进行内容交换)
  3. 3322(免费送二级域名)
  4. FreeDDns(免费送二级域名)
  5. WingDNS(功能超全!)
  6. Meibu(二级域名免费,顶级域名收费)

由市场调研可见,DDNS相关市场已经相当成熟,用户也趋于饱和,所以本文适合不想使用以上平台提供商的服务而是喜欢DIY瞎搞的同学。通过云+社区的搜索结果可以看到(如下图所示),社区目前还没有手把手教你实现动态域名解析的教程,所以本文主要的目的是使用python实现免费的动态域名解析能力,因为应用腾讯云的云API功能提高开发效率,所以云API这一开发神器值得进一步的推广。

云+社区

实现方法

材料准备

首先你需要有一个解析在腾讯云上的域名(我确实只有一个),如下图所示:

DNSPod

然后点击右上角的头像,进入API密钥页面,如下图所示:

点击API密钥

在腾讯云API密钥界面找到APPID对应的IDKey即可,如下图所示:

创建DNSPod步骤

保存好你的密钥,以备后用!本文在所有出现SecretID的地方,统一使用ID代替;在所有出现密钥SecretKey的地方,统一使用Key代替。

全过程前提假定

本文所有的实验方法和实验结果均基于以下假定:

  1. 假定你当前的IP就是目前需要动态解析的IP
  2. 假定目标域名已经备案,不会被阻断访问
  3. 假定读者具有一定的编写python能力
  4. 假定读者对本文出现的名词都比较熟悉

基于API Explorer的DDNS请求框架

首先登陆腾讯云,查阅DNSPod的相关文档,文档地址:https://cloud.tencent.com/document/product/1427/56194。里面列举了一系列的相关的接口列表,我们可能需要用到以下相关的API接口,如下表所示:

API名称

描述

DescribeRecordList

获取域名的解析记录

CreateRecord

添加记录

ModifyRecord

修改记录

ModifyDynamicDNS

更新动态 DNS 记录

我的理解是,第三个接口是第一个接口和第二个接口的组合:先获取已有的解析列表,然后查找是否有相应的子域名存在解析记录,如果存在则对该子域名的记录值进行修改,如果不存在则增加一条新记录。下面对结合腾讯云API Explorer对第三个接口进行实现,云API Explorer的地址为:ModifyDynamicDNS。操作界面如下图所示:

API Explorer操作界面

观察必需参数,发现有一项参数RecordId为待修改的记录ID,这项参数通过DescribeRecordList接口获取,如果我需要直接修改已有的记录时,记录需要修改的记录ID,后调用ModifyDynamicDNS接口;如果需要新建一个记录,并动态更新创建得到记录值时,可以先使用CreateRecord接口,记录创建好的记录ID,直接在ModifyDynamicDNS接口中使用。

本示例为动态修改已有的记录,因此结合DescribeRecordList接口和ModifyDynamicDNS接口实现域名动态解析能力。首先从DescribeRecordList接口文档页面进入API Explorer界面,配置好相关参数,如下图所示:

获取记录列表的接口

配置好参数后,右侧会生成相应的请求代码,本例教程使用的是Python SDK,因此复制python代码到本地,新建一个DDNS.py文件,如下图所示:

API代码示例

其中有涉及到腾讯云公共请求模块tencentcloud.common,该模块可以通过pip命令安装,文档地址:python SDK,安装命令如下:

代码语言:txt
复制
$ pip install --upgrade tencentcloud-sdk-python

我们将上图第9行的"SecretId", "SecretKey"改为之前我们保存的IDKey,直接运行得到一大堆JSON,这里展示正常相应返回的Response对象的部分属性:

代码语言:txt
复制
"Response": {
  "RequestId": "a3bee0a2-bac6-43db-b76d-a4ce11cec0c7",
  "RecordCountInfo": {
    "SubdomainCount": 23,
    "TotalCount": 23,
    "ListCount": 23
  },
  "RecordList": [
    {
      "Value": "59.52.241.247",
      "Status": "ENABLE",
      "UpdatedOn": "2021-08-13 20:03:43",
      "Name": "homesource",
      "Line": "默认",
      "LineId": "0",
      "Type": "A",
      "Weight": null,
      "MonitorStatus": "",
      "Remark": "",
      "RecordId": 760223447,
      "TTL": 600,
      "MX": 0
    }
  ]
}

其中名字为homesource的记录为我们需要进行更新的记录,其对应得到RecordId760223447。我们保存这个值,以待备用。

然后如法炮制,将获得的RecordId代入到ModifyDynamicDNS接口中,该接口的API Explorer地址为:ModifyDynamicDNS如下图所示:

修改DNS

经过测试发现SubDomain参数应该是必选项,如果不加此参数,该接口会默认修改@主机记录的参数值。将生成的代码复制到本地,去掉重复的模块导入剩下的部分如下图所示:

DDNS接口的本地代码

将你的IDKey替换代码中的"SecretId", "SecretKey",直接运行代码,请求结果如下所示:

代码语言:txt
复制
{
  "RecordId": 760223447,
  "RequestId": "2542afb0-bc7c-4c2e-b864-ca1d73795cdf"
}

回到DNSPod控制台查看API操作结果,如下图所示:

DDNS运行结果

可以看到该记录的值已经成功修改为了127.0.0.1,在实际应用中将该IP地址修改为其他IP地址即可,基于API Explorer的DDNS请求框架已经搭建好了,下面基于渐进式应用模式做进一步优化。

基于DDNS请求框架实现自动域名解析

由于当前的内容只有简单的框架,为了使它更加易用需要增加更多多内容。

自动获取指定子域名的RecordId

在上文中,我们是通过在大量的解析记录中肉眼查找需要修改的解析记录RecordId。但是这种方式的效率低下,这里实现根据请求结果,自动返回指定的子域名RecordId

实现逻辑:

  1. 申明变量,指定需要获取RecordId的主机记录值
  2. 遍历请求结果,找到Name与指定的主机记录相同的记录,返回RecordId
  3. 将模块函数化,便于调用

经修改后的获取域名的记录列表的代码改为如下所示:

代码语言:txt
复制
def ReturnRecordId(Domain, SubDomain):
    try:
        cred = credential.Credential(SecretId, SecretKey)
        httpProfile = HttpProfile()
        httpProfile.endpoint = "dnspod.tencentcloudapi.com"
    clientProfile = ClientProfile()
    clientProfile.httpProfile = httpProfile
    client = dnspod_client.DnspodClient(cred, "", clientProfile)

    req = models.DescribeRecordListRequest()
    params = {
        "Domain": Domain
    }
    req.from_json_string(json.dumps(params))

    resp = client.DescribeRecordList(req)
    for record in resp.RecordList:
        if record.Name == SubDomain:
            return record.RecordId
    print("未找到对应的记录值,请先创建相应的主机记录!")
    return -2

except TencentCloudSDKException as err:
    print("获取域名的记录列表失败,请重试!")
    return -1</code></pre></div></div><h4 id="77u2n" name="ModifyDynamicDNS%E6%8E%A5%E5%8F%A3%E5%B0%81%E8%A3%85%E6%88%90%E5%87%BD%E6%95%B0%EF%BC%8C%E4%BE%BF%E4%BA%8E%E8%B0%83%E7%94%A8">ModifyDynamicDNS接口封装成函数,便于调用</h4><p>封装好的<code>ModifyDynamicDNS</code>函数如下所示:</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>txt</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-txt"><code class="language-txt" style="margin-left:0">def ModifyDynamicDNS(RecordId, Domain ,SubDomain, Ip):
try:
    cred = credential.Credential(SecretId, SecretKey)
    httpProfile = HttpProfile()
    httpProfile.endpoint = &#34;dnspod.tencentcloudapi.com&#34;

    clientProfile = ClientProfile()
    clientProfile.httpProfile = httpProfile
    client = dnspod_client.DnspodClient(cred, &#34;&#34;, clientProfile)

    req = models.ModifyDynamicDNSRequest()
    params = {
        &#34;Domain&#34;: Domain,
        &#34;SubDomain&#34;: SubDomain,
        &#34;RecordId&#34;: RecordId,
        &#34;RecordLine&#34;: &#34;默认&#34;,
        &#34;Value&#34;: Ip
    }
    req.from_json_string(json.dumps(params))

    resp = client.ModifyDynamicDNS(req)
    if str(RecordId) in resp.to_json_string():
        print(&#34;更新成功!&#34;)
    return 1
except TencentCloudSDKException as err:
    return 0</code></pre></div></div><h4 id="bf799" name="%E5%AE%9A%E6%97%B6%E8%8E%B7%E5%8F%96%E5%BD%93%E5%89%8D%E7%9A%84IP%E5%9C%B0%E5%9D%80">定时获取当前的IP地址</h4><p>实现动态DNS解析,首先要获得当前本地的公网IP,然后将改IP提交到相应的API中。目前有很多免费公共的本地IP查询接口,这里我们选择的是:<code>https://ip.tool.lu/</code>,这个网站返回的结果更快,但是其返回的结果不是标准的JSON或其他标准数据格式,如下所示:</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>txt</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-txt"><code class="language-txt" style="margin-left:0">当前IP: 59.52.217.194 归属地: 中国 江西 南昌</code></pre></div></div><p>因此首先我们对该数据进行处理,提取IP地址。</p><p>然后,在获得IP地址后与先前的IP地址进行对比,判断IP是否发生变化,如果发生变化则将变动通过API提交。IP检查每隔一段时间运行一次,保证IP检测全方位无死角!这里用正则表达式和request模块完成IP提取方法,代码如下所示:</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>txt</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-txt"><code class="language-txt" style="margin-left:0">import requests

import re
def GetCurrentIP():
resp = requests.get('https://ip.tool.lu/').content
resp = resp.decode('utf8')
IPPattern = '\d+.\d+.\d+.\d+'
matchObj = re.search(IPPattern, resp)
return matchObj.group()

ip = GetCurrentIP()

每隔一段时间检查一下当前的IP是否与之前的IP相同,这里指定的时间间隔为10分钟,实现代码如下图所示:

代码语言:txt
复制
import time
interval = 600 # 每10分钟检查一次IP
OldIP = ""
while True:
CurrentIP = GetCurrentIP()
if OldIP != CurrentIP:
res = ModifyDynamicDNS(RecordId=RecordId, Domain=Domain, SubDomain=SubDomain, Ip = CurrentIP)
if res:
print(f'IP成功更新!原IP:{OldIP},新IP:{CurrentIP}')
OldIP = CurrentIP
else:
print('动态域名解析API出问题了,正在重试!')
continue
time.sleep(interval)

由该逻辑可以看出,当程序的第一次运行时,原IP是不显示的,但是当IP发生修改后,原IP就能正常显示。整理以上的IP更新逻辑,将其更新到DDNS.py文件中,其中主函数代码如下所示:

代码语言:txt
复制
if __name__ == "__main__":
SecretId = ""
SecretKey = ""
Domain = "eatrice.cn" # 主域名
SubDomain = "homesource" # 指定要修改的子域名
interval = 600 # 每10分钟检查一次IP
RecordId = ReturnRecordId(Domain=Domain, SubDomain=SubDomain)
if RecordId == -1:
print("RecordList请求发生问题!")
exit()
if RecordId == -2:
print("没有找到你要的子域名,请先新建一个!")
OldIP = ""
while True:
CurrentIP = GetCurrentIP()
if OldIP != CurrentIP:
res = ModifyDynamicDNS(RecordId=RecordId, Domain=Domain, SubDomain=SubDomain, Ip = CurrentIP)
if res:
print(f'IP成功更新!原IP:{OldIP},新IP:{CurrentIP}')
OldIP = CurrentIP
else:
print('动态域名解析API出问题了,正在重试!')
continue
time.sleep(interval)

至此,基于API Explorer的本地实现动态域名解析的教程已经全部完成。完整源代码已经开源至GitHub,地址:QiQiWan/PythonDDNS

小结

家用IP是动态的确实比较烦人,但是可以结合DNSPod实现曲线救国,实践证明,办法总比困难的多!另外值得一提的是API Explorer真是神器,妈妈再也不用担心在做API请求的时候发生问题了,省了很多事儿,真不错!