动态 DNS(DDNS)只负责在地址变化后更新 DNS 的 A 或 AAAA 记录。它不会分配公网地址、穿透运营商级 NAT(CGNAT)、打开路由器或主机防火墙,也不会让一个没有监听或认证的服务变安全。先证明网络路径可达,再选择更新方式;否则“域名解析正确”仍可能完全无法连接。
Table of Contents
1. 先画清楚 DDNS 所处的位置
| 层 | 要回答的问题 | DDNS 能否解决 |
|---|---|---|
| DNS | 主机名当前应指向哪个 IPv4/IPv6 地址 | 能,更新 A/AAAA |
| 接入网络 | 路由器 WAN 是否真的持有公网 IPv4,或主机是否有可路由 IPv6 | 不能分配或改变地址 |
| NAT/CGNAT | 入站连接能否从互联网到达边界设备 | 不能穿透 |
| 路由与防火墙 | 流量是否被明确转发并允许到正确主机/端口 | 不能自动开放 |
| 应用 | 服务是否监听、加密、认证、更新并限制暴露 | 不能替代应用安全 |
先确定目标:远程管理、家庭服务、监控回调或其他用途需要不同的风险评估。不要因为 DDNS 成功就盲目转发 SSH、数据库、管理面板或未经认证的服务。
2. 只读清点地址、路由、监听与现有更新器
在普通用户 shell 中先收集本机证据。ss 在无特权时可能不显示所有进程名;这应记录为权限边界,而不是“没有服务”。
ip -brief address
ip route show default
ip -6 route show default
ss -lntup
resolvectl status
systemctl list-timers --all | grep -i ddns
systemctl list-unit-files | grep -i ddns
另行记录路由器状态页显示的 WAN IPv4/IPv6 前缀、ISP 合同是否允许入站、DNS zone 的权威服务器、当前 A/AAAA/TTL,以及是否已有路由器、NAS、容器或主机更新器。不要在截图或工单中公开账号、token、TSIG key、完整配置导出、内部地址表或设备序列号。
3. 区分公网 IPv4、双重 NAT 与 CGNAT
RFC 1918 的 10.0.0.0/8、172.16.0.0/12、192.168.0.0/16 是私有 IPv4;RFC 6598 的 100.64.0.0/10 是运营商共享地址空间。若路由器 WAN 位于这些范围,或 WAN 地址与受信任的外部 HTTPS 地址检查结果不同,中间至少还有一层 NAT。也可能是上级自有路由器造成的双重 NAT,而不一定是 ISP CGNAT。
外部地址检查会把源地址和查询时间透露给检查服务。优先使用路由器/ISP 的受信任接口;若必须使用外部服务,选择明确授权、只返回一个地址、无 token 查询参数的 HTTPS endpoint。以下使用 IANA 保留的 .invalid 域名,故意不能运行;必须替换为已审阅的 endpoint。
PublicCheckUrl="https://ip-check.example.invalid/address"
case "$PublicCheckUrl" in
https://*) ;;
*) echo "Stop: public IP discovery must use HTTPS"; exit 1 ;;
esac
curl --proto '=https' --tlsv1.2 --fail --silent --show-error --max-time 15 "$PublicCheckUrl"
不要从搜索结果复制“显示 IP”页面并用正则抓取 HTML,也不要通过明文 HTTP 查询。页面结构、代理、重定向和多地址响应会造成错误;查询本身也有隐私成本。
4. IPv6 不是“没有 NAT 就一定可访问”
RFC 4193 的 fc00::/7 是 IPv6 Unique Local 地址,不是全球互联网的可路由地址。全局 IPv6 仍可能受 ISP 前缀轮换、临时隐私地址、路由器入站策略和主机防火墙影响。AAAA 应指向预定、稳定且被授权暴露的地址,不能随手选择 ip address 输出中的第一个 IPv6。
分别管理 A 与 AAAA。没有可用 IPv6 服务时不要保留陈旧 AAAA;客户端可能优先尝试它。若主机使用临时地址,给入站服务选择稳定地址/前缀方案并遵循发行版、网络管理器与 ISP 的当前文档,不要关闭隐私功能来掩盖架构问题。
5. 路由器更新还是主机更新:只能有一个 owner
| 情况 | 推荐 owner | 原因 |
|---|---|---|
| 路由器直接持有动态公网 IPv4 | 路由器原生 DDNS 功能 | 最先知道 WAN 变化,不依赖内网主机 |
| 服务器直接持有可路由地址 | 该服务器的单一受管 updater | 地址和记录 owner 一致 |
| DNS provider 只支持专用 HTTPS API | 受管 router/plugin 或受限 host service | 必须跟随 provider 当前 API |
| DNS 权威端支持 RFC 2136 | nsupdate + 受限 TSIG key |
标准协议,可审计并限制记录 |
| CGNAT 且没有入站 IPv6 | 反向 tunnel/受管 VPN/relay | DDNS 本身不创建入站路径 |
同一记录不能同时由路由器、主机、NAS 和容器更新;它们观察的地址或时序可能不同,造成记录来回抖动。为每个 FQDN、record type 和 updater 写明唯一 owner、更新触发、凭据、回退人和停用方式。
6. 凭据与更新协议边界
优先顺序是:设备原生且仍受维护的集成、provider 官方受限 client/API、或权威 DNS 明确支持的 RFC 2136。不要假定任意 provider 接受 nsupdate。HTTPS API 的 URL、method、认证 header、rate limit 和返回码必须来自 provider 当前官方文档,不能把 token 写进 URL、命令行、unit 的 Environment= 或日志。
RFC 2136 定义 DNS UPDATE;TSIG 的当前规范是 RFC 8945。BIND 文档明确不建议把 secret 放进 nsupdate -y,因为命令行和 shell history 可能暴露它。使用 -k 读取 key file,并让 DNS 管理端把 key 权限限制为单一名称和 A/AAAA 类型,而不是整个 zone。若 provider 只能发全局 API key,停止并寻求更窄权限或隔离账户。
每个 updater 使用独立凭据,文件仅由 root 读取,记录发放时间、scope、rotation 和 revoke 步骤。不要在示例、Git、聊天、故障日志或 systemd status 中粘贴 secret。
7. 可复现的 RFC 2136 IPv4 updater 范例
以下只是 provider-neutral 模板,适用于权威端明确支持 RFC 2136、该名称只有一个受此 updater 管理的 A RRset、并已发放最小权限 TSIG key 的情况。.invalid 值是故意失效的占位符,必须逐项替换。样例不自动处理 AAAA,避免把临时 IPv6 错当稳定服务地址。
把下面保存为本地工作副本 ddns-update,先做代码审阅。它拒绝非 HTTPS 地址检查、验证结果是 global IPv4、只在权威答案不含新地址时更新,并通过临时文件把命令交给 nsupdate -k;secret 不进入 argv。
#!/bin/sh
set -eu
: "${DNS_SERVER:?}"
: "${DNS_ZONE:?}"
: "${DNS_NAME:?}"
: "${IP_DISCOVERY_URL:?}"
: "${CREDENTIALS_DIRECTORY:?}"
case "$IP_DISCOVERY_URL" in
https://*'?'*|https://*'#'*|https://*'@'*) echo "IP discovery URL must not contain credentials or a query" >&2; exit 1 ;;
https://*) ;;
*) echo "IP discovery URL must use HTTPS" >&2; exit 1 ;;
esac
case "$DNS_SERVER$DNS_ZONE$DNS_NAME" in
*[!A-Za-z0-9._-]*) echo "DNS values contain unsupported characters" >&2; exit 1 ;;
esac
case "$DNS_ZONE:$DNS_NAME" in
*.:*.) ;;
*) echo "DNS zone and name must be absolute" >&2; exit 1 ;;
esac
test -r "$CREDENTIALS_DIRECTORY/tsig.key" || { echo "TSIG credential is not readable" >&2; exit 1; }
PublicIPv4="$(curl --proto '=https' --tlsv1.2 --fail --silent --show-error --max-time 15 "$IP_DISCOVERY_URL")"
if ! python3 - "$PublicIPv4" <<'PY'
import ipaddress
import sys
try:
value = ipaddress.ip_address(sys.argv[1])
except ValueError:
raise SystemExit(1)
raise SystemExit(0 if value.version == 4 and value.is_global else 1)
PY
then
echo "IP discovery did not return one global IPv4 address" >&2
exit 1
fi
if dig @"$DNS_SERVER" "$DNS_NAME" A +short | grep -Fxq "$PublicIPv4"; then
echo "DDNS record already current"
exit 0
fi
umask 077
UpdateFile="$(mktemp)"
trap 'rm -f "$UpdateFile"' EXIT HUP INT TERM
cat >"$UpdateFile" <<EOF
server $DNS_SERVER
zone $DNS_ZONE
update delete $DNS_NAME A
update add $DNS_NAME 300 A $PublicIPv4
send
EOF
nsupdate -k "$CREDENTIALS_DIRECTORY/tsig.key" "$UpdateFile"
echo "DDNS record updated"
300 只是管理员选择的示例 TTL,不是速度保证。先核对 provider/权威服务器允许的最小 TTL 和更新频率。不要用这段脚本管理已有多值、负载均衡、health-check、CDN、DNSSEC automation 或其他 controller 共同拥有的 RRset。
8. 安装文件,但暂不启动
先确认 curl、python3、dig、nsupdate 的实际路径和版本。Ubuntu 包名会随发行版变化;用当前 Ubuntu package index/apt-cache 核对,不要从随机脚本安装。DNS 管理员应通过安全渠道交付适用于 nsupdate -k 的 key file;不要在终端中手打 secret。
command -v curl python3 dig nsupdate
curl --version
python3 --version
dig -v
nsupdate -V
sudo install -d -m 0750 -o root -g root /etc/ddns
sudo install -m 0600 -o root -g root PATH_TO_ISSUED_KEY_FILE /etc/ddns/tsig.key
sudo install -m 0755 -o root -g root ddns-update /usr/local/libexec/ddns-update
PATH_TO_ISSUED_KEY_FILE 是必须替换的本地路径,不是 key 内容。安装后检查 owner/mode;不要把 /etc/ddns 加入 Git、备份到公共对象存储或复制到多台机器。
9. systemd oneshot service
保存为本地工作副本 ddns-update.service。四个 .invalid 配置必须用经审核的实际值替换;IP endpoint 不能在 query string 中携带 credential。LoadCredential= 在 service 启动时提供 key,DynamicUser= 和 hardening 限制进程的长期身份及文件系统范围。
[Unit]
Description=Update one dynamic DNS A record
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
DynamicUser=yes
Environment=DNS_SERVER=nsupdate.example.invalid
Environment=DNS_ZONE=example.invalid.
Environment=DNS_NAME=home.example.invalid.
Environment=IP_DISCOVERY_URL=https://ip-check.example.invalid/address
LoadCredential=tsig.key:/etc/ddns/tsig.key
ExecStart=/usr/local/libexec/ddns-update
NoNewPrivileges=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectSystem=strict
ProtectHome=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
LockPersonality=yes
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
这不是通用 provider API unit。若使用 HTTPS update API,应使用 provider 官方 client 或另外设计读取 systemd credential 的 wrapper;不要把 bearer token 伪装成这里的 TSIG file。
10. systemd timer 与单 owner 频率
保存为 ddns-update.timer。timer 使用启动后延迟、两次运行间隔和随机延迟,避免每台机器恰好同时请求;具体间隔仍必须服从 provider rate limit 和地址变化特征。
[Unit]
Description=Schedule dynamic DNS updates
[Timer]
OnBootSec=5min
OnUnitInactiveSec=15min
RandomizedDelaySec=90s
AccuracySec=30s
Unit=ddns-update.service
[Install]
WantedBy=timers.target
timer 不能替代 DHCP/网络事件通知,也不保证变更后恰好多少秒可见。短轮询会增加 provider 负载和隐私日志;长轮询会扩大旧地址窗口。记录选择依据。
11. 先静态验证,再单次运行
确认工作副本无 .invalid、scope 正确且 key 可撤销,再验证 unit。只有 systemd-analyze verify 通过后才安装;安装 timer 也不要立即 enable。
if grep -R -nF '.invalid' ddns-update ddns-update.service ddns-update.timer; then
echo "Stop: replace and review every .invalid placeholder"
exit 1
fi
systemd-analyze verify ./ddns-update.service ./ddns-update.timer
sudo install -m 0644 -o root -g root ddns-update.service /etc/systemd/system/ddns-update.service
sudo install -m 0644 -o root -g root ddns-update.timer /etc/systemd/system/ddns-update.timer
sudo systemctl daemon-reload
sudo systemctl start ddns-update.service
systemctl status ddns-update.service --no-pager
journalctl -u ddns-update.service --since today --no-pager
这里 grep 有输出意味着仍有占位符,应停止;无输出才继续。首次运行必须在维护窗口,保留 provider 控制台、旧 RRset/TTL、第二管理路径和撤销 key 的权限。不要在生产唯一入口上直接试验。
12. 从权威端、递归端与外部网络验证
把大写占位符替换为审核过的值。权威查询证明 zone 已更新;递归查询展示某个 resolver 当前 cache;它们不是同一证据。
dig @AUTHORITATIVE_SERVER HOSTNAME A +noall +answer
dig @RECURSIVE_RESOLVER HOSTNAME A +noall +answer
dig @AUTHORITATIVE_SERVER HOSTNAME AAAA +noall +answer
getent ahosts HOSTNAME
systemctl list-timers ddns-update.timer --all
然后从授权的外部网络测试应用层 TLS、认证和预定端口;同一 LAN 的 hairpin NAT 测试不能证明公网可达。不要使用公共“端口扫描”站点检查敏感服务,也不要把认证失败日志、完整域名清单或地址历史公开。
只有上述检查、外部测试和变更审批都通过后,才启用 timer:
sudo systemctl enable --now ddns-update.timer
systemctl list-timers ddns-update.timer --all
13. TTL、cache 与更新结果
RFC 1035 规定 TTL 是 RR 可缓存的时间上限;RFC 2308 还定义 negative caching。更新权威记录不会清除世界各地 resolver 已有的正缓存或 NXDOMAIN 缓存。客户端、浏览器、操作系统和应用也可能有额外 cache 行为。
因此不要承诺“更新后立即生效”。保留旧 endpoint 一段经过设计的重叠窗口(若安全且可控),分别查看权威答案和多个受控 recursive resolver,并根据旧 TTL/negative TTL 判断等待时间。反复删除/重建名称可能制造新的 negative cache;不要用不断刷新 DNS 作为故障修复。
14. NAT、防火墙与应用安全是独立变更
DNS 正确后仍需单独验证:边界设备是否持有目标地址、是否有明确且最小的转发/允许规则、主机 firewall 是否匹配 IPv4/IPv6、服务是否只监听预期 interface/port,以及 TLS、强认证、更新、rate limit 和审计是否就绪。
Ubuntu 官方文档说明 ufw 是 host firewall frontend,但不要因本指南运行 ufw allow。先用 sudo ufw status verbose、管理员审核的 nftables/路由器只读视图和 ss 收集证据,再通过独立变更单精确开放来源、protocol、port。数据库、容器 socket、router UI 和未认证 dashboard 不应直接暴露互联网。
15. 日志、监控与隐私
- 只记录时间、成功/失败类别、record name 的必要部分和 provider request ID;通常无需记录 secret、认证 header 或完整响应 body。
- 公共 DNS 会暴露主机名与地址;地址变化历史可能透露在线时间和网络迁移。选择不含姓名、住址或设备类型的名称。
- 不启用
curl -v、nsupdate -d/-D或 shell tracing 作为长期设置;它们可能扩大网络和认证元数据。 - 对连续失败、认证拒绝、地址快速抖动和 owner 冲突报警,但设置退避,避免触发 rate limit。
- 轮换或撤销 credential 后做单次验证;不要把旧 key 留在镜像、snapshot 或日志附件中。
监控应区分“发现地址失败”“更新被拒”“权威已更新”“recursive cache 仍旧”“网络路径不通”和“应用认证失败”。仅监控 DNS 解析会遗漏最重要的失败层。
16. 回退与故障矩阵
| 现象 | 可能层 | 安全动作 |
|---|---|---|
| updater 看到私有/共享 IPv4 | double NAT/CGNAT | 停止 A 更新,确认 ISP/上级路由器 |
| 权威 A 正确但外部无法连接 | NAT/firewall/service | 不再改 DNS,检查路径和监听 |
| 权威已新、recursive 仍旧 | TTL/cache | 等待已发布 TTL,查询受控 resolver |
| A 可用、AAAA 超时 | IPv6 route/firewall/陈旧 AAAA | 停止 AAAA owner,按变更流程修复或恢复 |
| 记录在两个地址间跳动 | 多 updater owner | 停用全部自动 owner,选定唯一 owner 后恢复 |
NOTAUTH/REFUSED/认证失败 |
zone/server/key scope | 不扩大 key 权限,交给 DNS 管理员 |
| 日志出现 secret | credential incident | 停止服务、撤销 key、限制日志并评估暴露 |
首先停止 timer,保留证据,再恢复 provider 控制面中记录的旧 RRset;不要在未知状态下删除 zone 或重置整个 DNS 账户。
sudo systemctl disable --now ddns-update.timer
sudo systemctl stop ddns-update.service
systemctl is-enabled ddns-update.timer
systemctl is-active ddns-update.service
journalctl -u ddns-update.service --since today --no-pager
确认旧记录、TTL、外部连接和唯一 owner 都恢复后,再决定保留还是移除 unit/key。若 key 可能曝光,先在 DNS 端撤销并签发新 key,不要复用。
17. 何时改用反向 tunnel、VPN 或 relay
当 IPv4 在 CGNAT 后、没有可入站 IPv6、ISP 阻断入站、无法安全管理边界 firewall,或服务不应直接公开时,DDNS 不是正确的连接方案。选择能由内网向受控 endpoint 发起出站连接的反向 tunnel、overlay VPN 或 authenticated relay,并单独评估信任、加密、身份、访问控制、日志、可用性、成本和退出迁移。
VPN/tunnel 也不是“自动安全”:控制面账户、device key、ACL、DNS leak、relay jurisdiction 和失效回退都需要管理。不要同时保留未经审计的公网端口转发。若用途只是远程管理,优先限制到明确身份和设备,而不是把管理端口公开给整个互联网。
如果你已经有一台 relay 和几台自己的电脑,只想在动网络规则前请人复核拓扑、监听边界与回退方案,我提供一项固定 250 美元的 LazyRemote Network Fit Review。先发不含密码、私钥或未脱敏配置的基本信息即可;这项服务不包含部署或硬件。
18. 当前协议与官方资料
- RFC 2136:DNS Dynamic Update
- RFC 8945:TSIG
- RFC 1035:DNS RR 与 TTL
- RFC 2308:DNS negative caching
- RFC 1918:私有 IPv4 地址
- RFC 6598:运营商共享地址空间 `100.64.0.0/10`
- RFC 4193:IPv6 Unique Local 地址
- ISC BIND 9:`nsupdate`、`-k` 与动态更新输入格式
- systemd:service 凭据与 sandbox 选项源码文档
- systemd:timer 源码文档
- systemd:service unit 源码文档
- curl:HTTPS、protocol 限制与失败处理
- Ubuntu Server:firewall
- IANA:示例与 `.invalid` 域名
资料核验于 2026-09-01。DNS provider API、router firmware、Ubuntu package 和 ISP 策略会变化;实施时使用所装版本和 provider/ISP 的当前官方文档。
19. 2014 年原文档案(仅供溯源)
以下外层围栏逐字保留 source_export 的完整可见正文;未做尾随空白规范化或私密值遮盖。档案包含明文 HTTP、credential-like 占位符、已过时 provider endpoint、把命令直接放入 cron 以及脆弱的 HTML/正则 IP 抓取方式。它们是惰性历史证据,不是现行指令;不要执行、复制或恢复这些 URL。
~~~~markdown
3322的更新还有更加简单的办法,连客户端都不用安装,也不用配置文件,不用知道IP。
www.3322.org网站上介绍的是用lynx(一般的Linux,BSD都自带),就可以了!
使用方法:
lynx -mime_header -auth=用户名:密码 "http://members.3322.net/dyndns/update?system=dyndns&hostname=域名"
而Ubuntu/Debian默认带的是w3m,所以要将命令改成:
w3m -no-cookie -dump http://username:password@members.3322.net/dyndns/update?system=dyndns&hostname=your_domain.f3322.org
把这条命令放在计划任务crontab(编辑用户的Crontab文件: crontab -e ,用户所建立的Crontab文件存于/var/spool/cron中,其文件名与用户名一致。 )里15分钟执行一次就能定时更新了。
*/15 * * * * w3m -no-cookie -dump 'http://username:password@members.3322.net/dyndns/update?system=dyndns&hostname=your_domain.f3322.org'>/dev/null
顺便,使用w3m查询自己公网IP的方法:
w3m -no-cookie -dump www.ip138.com|grep -o "[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}"
~~~~
