
简介对于使用公网IP宽带、因ISP重新分配导致IP频繁变动的家庭用户或小型工作室这份腾讯云DDNS脚本提供了自动同步域名解析的实用方案。脚本将动态变化的公网IP与固定域名绑定消除手动登录控制台修改解析记录的繁琐步骤适合有远程访问、个人网站或NAS公网接入需求的场景。资源包为zip压缩格式内含1个ddns.sh脚本文件整体仅2KB体量轻巧、便于部署。脚本面向Linux环境可搭配crontab定时任务周期运行在检测到公网IP变化后自动调用腾讯云API更新解析记录配置时只需填入API密钥、域名ID与子域名等参数即可Windows用户亦可参考其调用逻辑做移植。目前已有4244人学习下载。通过该脚本可快速搭建自有域名的DDNS自动化流程省去手动改IP的重复操作同时能直观理解腾讯云API的认证调用方式与DDNS更新机制对希望以低成本维护稳定公网入口的读者很有参考价值。1. 公网IP宽带做DDNS为什么我选腾讯云而不是路由器自带功能家里宽带是公网IP重启光猫或者运营商半夜重拨一次IP就悄悄变了。你人在外面域名还指着旧地址远程桌面、NAS、自建网站全连不上。腾讯云DDNS脚本要解决的就是这件事让域名解析记录跟着公网IP自动变你不用每次手改DNS。适合在Linux小主机、软路由、Windows闲置机器上跑尤其适合域名已经托管在腾讯云DNSPod的人。我折腾过路由器自带的DDNS最后换成腾讯云API脚本原因很直接自带功能只能绑定指定服务商更新频率被写死而脚本能自由控制域名、子域名、TTL还能批量更新。下面从原理到部署一步步讲清楚。2. 先搞懂DDNS更新链路的三个环节IP获取、API调用、定时触发DDNS不是魔法它只是一段重复劳动查一下当前公网IP跟域名解析记录里的IP比一比不一样就调API改掉。这三个环节分别对应“IP getter”“API caller”“scheduler”任何一个环节出错整个链路就断。2.1 公网IP会变是常态先确认你的宽带真的是公网IP很多用户以为宽带是公网IP实际上运营商给的是大内网地址CGNAT网段比如100.64.0.0/10。这种情况下你从路由器WAN口看到的IP和外部服务看到的IP对不上DDNS脚本再努力也没用因为出口IP根本不是你的。第一步先确认自己有没有资格做DDNS。我一般用两个办法确认。第一个登录光猫或路由器看WAN口IP然后手机切4G访问ip.sb或ifconfig.me对比两个值是否一致。第二个直接在路由器或内网机器上执行一条命令curl -4 --connect-timeout 5 --retry 1 https://api.ip.sb/ip如果输出的是100.64.x.x、172.16.x.x这类私有或运营商保留地址基本可以断定不是公网IP如果输出的是公网IP段且和路由器WAN口一致那就可以继续。注意现在很多光猫默认拨号路由器拿到的反而是内网IP需要把光猫改成桥接模式、由路由器拨号这一步没做后面全白搭。确认拿到公网IP后还要确认一件事你的IP是动态的。现在家庭宽带基本都是动态公网IP运营商不会保证固定48小时或一定流量后强制重拨换IP很常见。所以DDNS脚本的更新频率要按“一段时间内IP可能变一次”来设计没必要每秒查一次浪费API配额。2.2 腾讯云DNS API的工作原理从SecretId到RecordId的映射腾讯云DDNS脚本本质是一个HTTPS客户端向腾讯云DNSPod的API接口发请求修改解析记录。最核心的API是两个DescribeRecordList用来查现有解析记录拿到每条记录的RecordIdModifyRecord或UpdateDynamicDNS用来把记录值改成当前公网IP。为什么需要RecordId因为腾讯云API更新记录时要求传唯一标识域名加子域名不够得用数字ID定位。API调用需要身份认证。腾讯云统一用的是SecretId和SecretKey做HMAC-SHA256签名然后放到请求头。Python SDK已经封装好了签名过程你不用自己去拼签名。流程是这样的脚本用Credential对象传入一对密钥创建DnspodClient然后调用DescribeRecordList从返回的JSON里解析出RecordId再带着新IP去调用ModifyRecord。如果解析记录不存在还得先CreateRecord创建一条这是老手容易忽略的边界情况。这里要记住两个关键参数RecordType和RecordLine。RecordType指A记录IPv4还是AAAA记录IPv6RecordLine默认是“默认”但如果你之前在DNSPod控制台选过“联通”“电信”等线路脚本更新时必须带上同样的线路值否则会覆盖成默认线路导致特定运营商用户解析异常。我一开始没传RecordLine结果联通线路的解析被改成默认排查了半天。2.3 定时触发为什么不用路由器而是用Linux cron或Windows计划任务路由器自带DDNS服务一般只支持花生壳、No-IP这类第三方服务商即使有人把腾讯云刷进固件更新策略也是固定的。而自己写脚本的好处是可以自由控制触发间隔、失败重试、日志记录甚至能用systemd timer实现“错过补跑”。常见做法是放在内网一台一直开机的设备上比如树莓派、NAS、旧笔记本用Linux cron每5分钟检查一次IP变了才更新没变就退出这样既不打扰API又能在IP变化后约5分钟内完成解析更新。Windows用户也简单用计划任务调用python.exe加参数即可。我建议用cron而不是更短的间隔因为腾讯云API的访问频率限制是每秒几十次家用场景每5分钟一次完全够。真正决定更新时效的不是脚本跑多勤而是解析记录的TTL腾讯云DNSPod的TTL最低可以设120秒建议设600秒这样IP变了以后互联网上最多10分钟能看到新地址。3. 用Python写一个腾讯云DDNS脚本从零到跑通最小命令这一章直接上可运行的代码。我用Python写因为腾讯云SDK对Python支持最好而且代码短、易改。如果你更习惯Shell也有用curl直接调API的写法但签名流程相对繁琐Python能省掉很多低级错误。3.1 准备腾讯云API密钥子账号授权与权限边界先去腾讯云控制台开通云解析DNS服务。然后不要再点开主账号的API密钥了而是创建一个子用户只授予操作DNSPod的权限。这样做有两个好处第一密钥泄露时作用域有限第二权限边界清楚以后别人接手也方便。在访问管理控制台创建用户选择“编程访问”创建后得到SecretId和SecretKey。然后给这个子用户关联策略我常用的是自定义策略只允许dnspod:DescribeRecordList、dnspod:ModifyRecord、dnspod:CreateRecord。注意策略里要写允许的资源范围比如指定某个域名ID。这样即使密钥被拿到也只能改你授权的那几个域名影响可控。拿到密钥后不要直接写进代码我习惯放在~/.tencentcloud/credentials文件里或者用环境变量传入。下面这个脚本用配置文件读取方便以后在多个机器间复制。3.2 安装SDK并实现核心更新逻辑先安装腾讯云DNSPod的Python SDK版本用最新的即可。安装命令pip3 install tencentcloud-sdk-python-dnspod tencentcloud-sdk-python-common安装成功后写一个最小可用的脚本。下面的代码做了三件事获取公网IP、查询现有记录ID、更新记录值。#!/usr/bin/env python3 # -*- coding: utf-8 -*- import os, sys, json, logging import requests from tencentcloud.common import credential from tencentcloud.common.profile.client_profile import ClientProfile from tencentcloud.common.profile.http_profile import HttpProfile from tencentcloud.dnspod.v20210323 import dnspod_client, models # 读取配置文件格式: SecretIdxxx SecretKeyxxx def load_conf(path): conf {} with open(path, r) as f: for line in f: if in line and not line.strip().startswith(#): k, v line.strip().split(, 1) conf[k.strip()] v.strip() return conf def get_public_ipv4(): # 用多个源防止单个服务挂掉影响更新 urls [https://api.ip.sb/ip, https://api.ipify.org, https://4.ipw.cn] for u in urls: try: r requests.get(u, timeout5) if r.status_code 200: ip r.text.strip() # 简单校验是不是IPv4 if ip.count(.) 3 and all(0 int(x) 255 for x in ip.split(.)): return ip except Exception: continue return None def update_record(cred_obj, domain, subdomain, record_type, value, record_line默认): client dnspod_client.DnspodClient(cred_obj, ) # 第一步: 查询记录列表找到对应的RecordId req models.DescribeRecordListRequest() req.Domain domain req.Subdomain subdomain req.RecordType record_type resp client.DescribeRecordList(req) for r in resp.RecordList: if r.Name subdomain and r.Type record_type and r.Line record_line: record_id r.RecordId # 如果记录值已经一样不需要更新 if r.Value value: logging.info(fIP未变化: {value}跳过更新) return False, NO_CHANGE # 第二步: 修改记录 mod_req models.ModifyRecordRequest() mod_req.Domain domain mod_req.SubDomain subdomain mod_req.RecordType record_type mod_req.RecordLine record_line mod_req.Value value mod_req.RecordId record_id client.ModifyRecord(mod_req) logging.info(f更新成功: {subdomain}.{domain} - {value}) return True, UPDATED # 记录不存在需要创建 create_req models.CreateRecordRequest() create_req.Domain domain create_req.SubDomain subdomain create_req.RecordType record_type create_req.RecordLine record_line create_req.Value value client.CreateRecord(create_req) logging.info(f记录不存在已创建: {subdomain}.{domain} - {value}) return True, CREATED if __name__ __main__: logging.basicConfig(levellogging.INFO, format%(asctime)s %(levelname)s %(message)s) conf load_conf(os.path.expanduser(~/.tencentcloud/ddns.conf)) cred credential.Credential(conf[SecretId], conf[SecretKey]) ip get_public_ipv4() if not ip: logging.error(无法获取公网IP请检查网络) sys.exit(1) # 下面两个值可以放到命令行参数这里先用常量 domain example.com subdomain home update_record(cred, domain, subdomain, A, ip)这段代码的核心逻辑是“先查后改”。为什么要先查因为你要拿到RecordId而且先判断值是否相同相同就跳过API调用省配额也避免频繁写操作触发限流。DescribeRecordList返回的RecordList里每一项都有Name、Type、Line、Value、RecordId我用这三者组合去匹配目标记录避免更新到别的子域名。注意req.Subdomain在腾讯云API里不传时是主机记录名这里传的是home如果你要更新根域名记录Subdomain要传不同SDK版本可能行为略微差异最好在控制台先手工建一条再调试。3.3 用命令行参数控制域名和记录类型一条命令更新一个A记录写死域名只适合单机单域名。更通用的是把域名、子域名、记录类型、TTL做成命令行参数这样一条命令可以控制多个解析记录。下面我重构一下入口部分支持参数化import argparse if __name__ __main__: parser argparse.ArgumentParser(description腾讯云DDNS更新脚本) parser.add_argument(--domain, requiredTrue, help顶级域名如 example.com) parser.add_argument(--subdomain, default, help主机记录默认表示主域名) parser.add_argument(--type, defaultA, choices[A, AAAA], help记录类型A为IPv4AAAA为IPv6) parser.add_argument(--ttl, typeint, default600, helpTTL单位秒建议600~3600) parser.add_argument(--line, default默认, help解析线路默认值适合绝大多数用户) args parser.parse_args() logging.basicConfig(levellogging.INFO, format%(asctime)s %(levelname)s %(message)s) conf load_conf(os.path.expanduser(~/.tencentcloud/ddns.conf)) cred credential.Credential(conf[SecretId], conf[SecretKey]) ip get_public_ipv4() if args.type A else get_public_ipv6() if not ip: logging.error(f无法获取{args.type}地址请检查网络和前缀) sys.exit(1) update_record(cred, args.domain, args.subdomain, args.type, ip, args.line)这里的get_public_ipv6是额外函数原理类似访问https://6.ipw.cn等接口。注意腾讯云API的SubDomain字段对于根域名是对于子域名是直接写home不要带点。--ttl参数目前没有传给ModifyRecordRequest腾讯云修改记录接口支持TTL字段你在实现时要补上用mod_req.TTL args.ttl。很多现成脚本不设置TTL导致默认值可能是7200秒IP变了要两小时才能全局生效所以这个参数很重要。3.4 增加日志与返回值校验脚本不能只执行不报错很多初版脚本跑一次没报错就以为成功了其实可能密钥错了、域名错了、记录类型不匹配只是接口返回的Error被忽略了。腾讯云SDK的接口调用如果失败会抛异常异常里带Code和Message这比HTTP状态码更直观。我建议在调用ModifyRecord时用try/except捕获并且把响应里的Error打印出来。再一个是日志。不要只打印到屏幕因为定时任务里的print输出要等任务结束才被捕获出错时很可能看不到。我一般用logging模块同时写到文件方便事后排查。改造方法如下logging.basicConfig( levellogging.INFO, filenameos.path.expanduser(~/ddns/ddns.log), format%(asctime)s %(levelname)s %(message)s )另外脚本退出码也有讲究。如果IP获取失败应返回非0退出码这样cron或systemd能感知失败。区分业务逻辑失败和API调用失败前者用sys.exit(1)后者用sys.exit(2)方便你的通知脚本判断。我用过一段时间后体会是DDNS脚本最大的问题是“静默失败”IP变了但脚本因某个外部接口超时没更新域名解析就停留在旧IP表面上脚本每天都在跑实际上已经不干活了。所以日志和退出码是保命环节。4. 把脚本部署成自动任务Linux cron 与 systemd timer 的完整配置脚本写好了最后一步是让它自动化。这里我给出Linux和Windows两种常见方案并且重点说cron/systemd timer的坑。4.1 Linux下用cron每5分钟执行一次最小配置cron是Linux上最成熟的定时服务配置简单。打开当前用户的crontabcrontab -e加入一行*/5 * * * * /usr/bin/python3 /home/pi/ddns/ddns.py --domain example.com --subdomain home --type A /home/pi/ddns/ddns.log 21这一行很简单但有几个坑。第一/usr/bin/python3要写绝对路径不能写python3因为cron的PATH很干净经常不包含/usr/local/bin而pip install的SDK可能装在/usr/local/lib/pythonX/site-packages你用which python3查一下。第二脚本所在目录要写绝对路径如果脚本内部用了相对路径读取配置文件cron的工作目录是用户家目录不是脚本目录很容易读到不存在的文件。第三重定向不能少否则cron执行出错时你连错误都看不到。cron的最小粒度是分钟*/5就是每5分钟跑一次这个频率对家庭DDNS足够。4.2 systemd timer 比 cron 强在哪错过补跑与日志查看如果你用的系统是Debian系新版或CentOS 7我更推荐systemd timer。它比cron好的地方在于可以设置Persistenttrue系统关机期间错过的时间会在开机后补跑还能用journalctl统一查看日志。下面给一个最小配置。创建一个/etc/systemd/system/ddns.service文件[Unit] DescriptionTencent Cloud DDNS Updater Afternetwork-online.target Wantsnetwork-online.target [Service] Typeoneshot ExecStart/usr/bin/python3 /home/pi/ddns/ddns.py --domain example.com --subdomain home --type A Userpi再创建一个/etc/systemd/system/ddns.timer文件[Unit] DescriptionRun DDNS Updater every 5 minutes [Timer] OnBootSec1min OnUnitActiveSec5min Persistenttrue [Install] WantedBytimers.target然后执行sudo systemctl daemon-reload sudo systemctl enable ddns.timer --now这里Typeoneshot表示服务执行一次就退出适合这种脚本任务。OnBootSec1min是开机延迟1分钟执行OnUnitActiveSec5min的含义是这个timer激活后每5分钟触发一次。Persistenttrue就是这个timer的灵魂如果上次触发时系统关机了下次开机后会立即补一次避免关机期间IP变化漏更新。查看日志用journalctl -u ddns.service -e比cron重定向到文件更干净。4.3 Windows用户怎么做计划任务与bat/powershell包装如果你的常开设备是Windows用任务计划程序也能达到同样效果。先把脚本封装成bat因为任务计划程序直接运行Python命令有时候弹窗加上pythonw可以避免窗口闪烁。新建ddns.batecho off C:\Python39\pythonw.exe C:\ddns\ddns.py --domain example.com --subdomain home --type A C:\ddns\ddns.log 21在任务计划程序里创建基本任务触发器选“每天”然后修改为“重复任务间隔5分钟”持续时间“无限期”。操作选择“启动程序”程序填C:\ddns\ddns.bat。注意勾选“使用最高权限运行”这一项虽然腾讯云API不需要管理员权限但有些系统下Python访问网络需要防火墙放行管理员运行能免去一些权限弹窗。还有一个Windows特有的坑计划任务默认只在用户登录时运行你要在条件标签里取消勾选“只有在计算机使用交流电源时才启动”并勾选“不管用户是否登录都要运行”。4.4 上线前先手动跑三遍确认幂等性部署完自动任务之前我强烈建议手动连续执行三次完全相同的命令。第一次会更新记录第二次因为IP没变应该跳过更新并打印NO_CHANGE第三次也一样。如果第二次、第三次反而又去调ModifyRecord说明你的脚本没有判断IP变化会导致每次定时任务都产生一次写操作日积月累容易被腾讯云限流。我见过有人写脚本不对比旧IP直接每次覆盖虽然功能上没毛病但API用量大了之后被限流的概率明显上升。手动测试时用一个测试子域名比如test.home.example.com避免把正在用的home.example.com改错。测试后再把定时任务指向正式子域名。这三遍测试下来基本能覆盖密钥错误、记录不存在、HTTP接口超时三种主要问题。5. 腾讯云DDNS脚本避坑手册5个让我半夜爬起来改脚本的坑这一章是我实际使用的血泪经验。每个坑都按“现象、原因、解决”写你可以直接对照排查。5.1 现象域名解析没变但脚本返回成功我在脚本里写死了RecordId第一次手动更新成功后来运营商重拨换了IP日志显示更新成功可DNS查询还是旧IP。原因腾讯云DNSPod的RecordId不是固定不变的。如果你在控制台手动修改过解析记录或者用过“暂停”“启用”功能RecordId可能被替换另外如果你有两台机器同时跑这个脚本用同一个RecordId会相互覆盖。解决每次更新前都重新查一次DescribeRecordList用域名、子域名、类型、线路四个字段去匹配而不是用缓存下来的RecordId。我的脚本里已经按这个方式写了如果你还在用固定ID赶紧改。5.2 现象更新IPv6时一直报错返回“记录值不合法”一开始我只更新A记录。后来家里网络支持IPv6想加一条AAAA记录脚本却一直报参数错误。原因腾讯云API的DescribeRecordList如果传了RecordTypeAAAA会过滤出AAAA记录但如果你在同一域名下同时存在A和AAAA记录查询返回的列表里可能混着其他类型我之前的代码用r.Name subdomain匹配却忘了判断r.Type导致拿到的RecordId是A记录的用去更新AAAA记录自然报错。解决在循环里同时判断r.Name、r.Type、r.Line确保拿到的是同一类型记录。代码里我已经加了r.Type record_type这是必需品。5.3 现象cron不执行手动跑却正常定时任务设置了但日志文件一直是空白的。手动执行却正常。原因cron环境变量很少PATH里没有python3而且我的脚本用了requests这个第三方库装在/home/pi/.local/libcron执行时用户是pi但PYTHONPATH没有包含用户本地site-packages导致import requests失败。解决在cron行里写绝对路径并在脚本开头强制指定Python解释器比如#!/usr/bin/env python3只对交互shell有用cron下最好直接写成*/5 * * * * /usr/bin/python3 /home/pi/ddns/ddns.py ...另外在ddns.py里可以用sys.path.insert(0, os.path.expanduser(~/.local/lib/python3.9/site-packages))强行加插路径但这比较脏。我后来改用systemd直接在service文件里指定EnvironmentPYTHONPATH/home/pi/.local/lib/python3.9/site-packages干净很多。5.4 现象脚本频繁报“请求被限流”腾讯云API返回错误码4500有一次我写了个死循环获取IP失败就重试IP获取接口偶尔超时脚本就在同一分钟内反复调用腾讯云API触发了每秒请求数限制。现象是脚本一连串报错日志里全是“ActionUnauthorized”或“RequestLimitExceeded”。原因腾讯云API对单个密钥有访问频率限制短时间大量请求会被暂时锁定。解决在脚本里加一个“只在IP变化时才调API”的闸门前面我已经实现了if r.Value value: return NO_CHANGE这个判断就在API调用前。另外IP获取失败时不要重试超过3次每次等待至少10秒。如果你用cron每5分钟跑一次加这个判断后绝大多数运行周期连API都不会碰自然不会被限流。5.5 现象脚本显示更新成功但浏览器访问域名还是旧IP更新完解析后我用手机浏览器访问home.example.com打开的还是旧网站但用nslookup查DNS解析结果却是新IP。原因两层缓存。第一层是本地电脑的DNS缓存Windows下ipconfig /flushdns可以刷新第二层是运营商递归DNS的缓存这个只能等TTL超时。还有一层更隐蔽如果你使用CDN或穿透隧道CDN节点本身有缓存不直接回源到你的域名。解决把解析记录的TTL改成600秒以内更新后不要急着刷新浏览器等10分钟再测。验证时不要用pingping可能走系统缓存用dig 8.8.8.8 home.example.com直接查权威递归服务器这才反映全局DNS状态。6. 更稳的DDNS配方一键验证、多域名批量更新、异常通知一个不落到这里基础DDNS已经能跑了但离“稳”还差三步更新后验证、批量管理多个域名、异常发生时通知人。这三步做好以后你甚至可以在IP变化后几分钟内收到一条“你的home.example.com已更新到223.5.5.5”的消息。先说验证。每次ModifyRecord成功后不要立刻退出了事。脚本应该等几十秒再执行一次DescribeRecordList读回记录值对比是否等于刚才写入的值。如果不等说明腾讯云后端还没生效或写入失败需要报错退出。这个验证不额外消耗API配额因为这是必要的读操作。再看批量更新。家里可能跑着NAS、软路由、RDP网关每个服务对应不同子域名。常见做法是把域名列表写进一个配置文件循环执行更新# ddns_targets.yaml records: - domain: example.com subdomain: home type: A line: 默认 - domain: example.com subdomain: nas type: A line: 默认 - domain: example.net subdomain: type: AAAA line: 默认脚本里用yaml.safe_load读这个文件循环调用update_record。注意每个记录更新前都要走一次“查询比较”不要用同一个IP去覆盖所有记录因为记录值可能对应不同网口比如IPv4和IPv6。这个文件可以放在~/ddns/目录和cron/service放在一起。最后是异常通知。我常用的方案是腾讯云“云监控”告警但对DDNS这种轻量任务更轻量的是用企业微信机器人Webhook。当脚本发生以下任一情况时发送一条消息到手机IP获取连续失败2次、API调用抛异常、验证返回值不一致。下面是一个简化的通知函数import requests def send_wecom(text): webhook os.environ.get(WECOM_WEBHOOK) if not webhook: return requests.post(webhook, json{msgtype: text, text: {content: text}}, timeout5)在except分支里调用send_wecom(fDDNS异常: {e})这样你不用每天看日志出问题手机会响。我用这个方案后再也不用半夜爬起来改脚本了——有一次光猫重启后IPv6前缀变了脚本自动更新AAAA记录并通知了我我在外面用手机确认远程桌面能连上全程没有登录服务器。这三年我维护这套DDNS方案最大的教训是“不要信任任何一个环节默认没问题”。IP获取接口可能换域名SDK版本升级可能改字段名腾讯云控制台手动改记录可能让脚本的缓存失效。所以我现在把“查询-比较-更新-验证”写成一条完整链路所有步骤都留痕。希望帮到你。如果你刚拿到公网IP别急着去装第三方穿透工具先把这个脚本跑起来你会发现自建服务的完整拼图一下子齐了。本文还有配套的精品资源点击获取