通过域名访问一台Ubuntu主机时,如果接入的是普通家庭宽带,运营商分配的公网IP通常是动态变化的。每次光猫或路由器重新拨号,IP都可能改变,导致之前设置的DNS A记录失效。要解决这个问题,需要引入DDNS(动态域名解析)机制,让主机在IP变化后主动通知DNS服务商更新解析记录。下面将围绕Ubuntu环境,逐步展示如何用Cloudflare提供的API实现一个轻量、可靠的DDNS更新方案。

一、DDNS的工作逻辑与方案选型
传统的DNS解析记录是静态的,一旦添加,通常不会自动变更。对于拥有固定公网IP的服务器,手动设置一次A记录就能长期使用;但家庭宽带的公网IP并不固定,运营商可能每隔一段时间重新分配地址,甚至每天变化多次。如果每次都要登录DNS管理后台手动修改,既麻烦又容易遗漏。
DDNS的思路是在目标主机上运行一个客户端,由它周期性地获取本机出口IP,并与上一次成功更新的IP进行对比。如果发现地址发生变化,就调用DNS服务商提供的API,把域名对应的A记录内容覆盖为新IP。为了减少不必要的API调用,客户端通常会缓存上次IP,仅在变化时发起更新请求。整个过程自动完成,用户无需干预。
在Ubuntu上实现DDNS有多种方式。ddclient是比较老牌的通用客户端,支持多种DNS服务商,但配置文件较为繁琐,而且部分插件更新滞后。自己编写脚本调用Cloudflare API则更简单直观,只需要curl和jq两个命令即可完成,适合希望了解底层细节或需要定制行为的用户。本文选择自定义脚本加systemd timer的组合,既保证轻量,也方便排查问题。
二、准备Cloudflare API凭据与编写更新脚本
开始之前,确保域名已经托管在Cloudflare上,并且已经为需要动态更新的子域名(例如home.ipipp.com)创建了一条A记录。A记录可以随便填一个IP地址,后续脚本会自动覆盖。接下来登录Cloudflare控制台,进入“我的个人资料”下的“API令牌”页面,创建一个新的API令牌。权限选择“区域- DNS-编辑”,区域资源限定为对应域名。这样即使令牌泄露,攻击者也仅能修改该域名的解析记录,不会影响其他服务。
创建完成后会显示一次令牌明文,务必及时保存。随后进入域名的概述页面,右侧可以看到Zone ID。在DNS管理页面,点击对应A记录,可以获取Record ID,它是一串十六进制标识符。将这三个值记录下来,后续写入脚本。为了安全,也可以将它们放入独立的配置文件中,并通过权限设置限制读取。
接下来安装必要的工具:curl用于发起HTTP请求,jq用于解析JSON响应。在Ubuntu上执行sudo apt update && sudo apt install curl jq -y即可。然后创建脚本文件/usr/local/bin/update_ddns.sh,内容如下。脚本首先通过多个IP查询接口依次尝试获取出口IP,这样可以避免单一服务不可用导致更新失败。获取成功后与缓存文件中的旧IP比较,只有发生变化才调用Cloudflare API。API响应中的success字段用来判断更新是否成功,成功后将新IP写入缓存。
更新脚本示例:
#!/bin/bash
# Cloudflare DDNS 更新脚本
API_TOKEN="填写你的API令牌"
ZONE_ID="填写Zone ID"
RECORD_ID="填写Record ID"
DOMAIN="home.ipipp.com"
get_ip() {
for url in "https://api.ipify.org" "https://ifconfig.me/ip" "https://icanhazip.com"; do
IP=$(curl -s --connect-timeout 5 "$url")
if [[ -n "$IP" ]]; then
if [[ "$IP" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "$IP"
return 0
fi
fi
done
return 1
}
IP=$(get_ip)
if [[ -z "$IP" ]]; then
echo "无法获取公网IP"
exit 1
fi
CACHE_FILE="/tmp/ddns_ip_cache"
if [[ -f "$CACHE_FILE" ]]; then
OLD_IP=$(cat "$CACHE_FILE")
else
OLD_IP=""
fi
if [[ "$IP" == "$OLD_IP" ]]; then
echo "IP未变化,跳过更新: $IP"
exit 0
fi
RESPONSE=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$RECORD_ID" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
--data "{\"type\":\"A\",\"name\":\"$DOMAIN\",\"content\":\"$IP\",\"ttl\":120,\"proxied\":false}")
if echo "$RESPONSE" | grep -q '"success":true'; then
echo "$IP" > "$CACHE_FILE"
echo "DDNS更新成功: $IP"
else
echo "DDNS更新失败: $RESPONSE"
exit 1
fi
脚本中的get_ip函数依次尝试三个公网IP查询接口,避免单一服务不可用;proxied参数设为false表示不经过Cloudflare代理,让域名直接解析到真实IP,适合远程桌面、SSH等非HTTP服务。保存后执行chmod +x /usr/local/bin/update_ddns.sh,再手动运行一次验证。如果输出“DDNS更新成功: 你的IP”,说明凭据和网络均正常。
三、配置systemd定时任务实现自动更新
手动执行脚本只能解决一次更新,要让域名持续跟随公网IP变化,需要借助Ubuntu的systemd定时器。systemd timer比传统cron更加灵活,可以指定开机后首次运行时间以及后续执行间隔,并且服务日志统一由journald管理,排查问题更方便。
首先创建服务单元文件/etc/systemd/system/ddns-update.service,将脚本注册为一个一次性服务。内容中指定Type=oneshot,表示任务执行完成后进程退出,不会驻留后台。After=network-online.target确保网络就绪后再执行,避免开机时因网络未连接导致IP获取失败。然后创建定时器单元/etc/systemd/system/ddns-update.timer,设置开机后2分钟首次触发,之后每5分钟运行一次。这个频率对于大多数动态IP场景足够,同时不会对Cloudflare API造成过大压力。
两个配置文件示例:
[Unit] Description=Update DDNS for Cloudflare After=network-online.target Wants=network-online.target [Service] Type=oneshot ExecStart=/usr/local/bin/update_ddns.sh User=root
[Unit] Description=Run DDNS update every 5 minutes [Timer] OnBootSec=2min OnUnitActiveSec=5min Unit=ddns-update.service [Install] WantedBy=timers.target
配置文件保存后,执行以下命令重新加载systemd并启用定时器:sudo systemctl daemon-reload、sudo systemctl enable --now ddns-update.timer。通过systemctl status ddns-update.timer可以查看定时器是否处于活跃状态,systemctl list-timers ddns-update.timer会显示下一次触发时间。手动测试时可以运行sudo systemctl start ddns-update.service,并用journalctl -u ddns-update.service -f实时查看日志输出。
为了验证整个链路是否正常,可以先删除/tmp/ddns_ip_cache缓存文件,再强制启动一次服务,观察Cloudflare控制台中的A记录是否更新为当前公网IP。如果IP没有变化,脚本会输出“IP未变化”,这同样说明检测逻辑有效。
四、常见问题排查与安全建议
配置过程中最容易遇到的问题是脚本无法获取正确的出口IP。如果Ubuntu主机位于路由器后面,通过NAT访问互联网,那么curl查询到的是路由器的公网IP,而不是内网地址,这正是我们需要的出口IP。但如果主机同时配置了代理环境变量,curl可能会走代理导致获取到错误的地址,可以在脚本中显式使用--noproxy参数或清除相关环境变量。
另一个常见原因是API令牌权限不足。Cloudflare令牌必须包含对应区域的DNS编辑权限,如果只给了读取权限,更新时会返回403错误。脚本中可以通过grep -q '"success":true'检测响应,如果失败,响应体中会包含具体的错误消息,通常与令牌或Zone ID、Record ID不匹配有关。建议先将脚本中的变量替换为实际值,手动执行一次,观察完整JSON输出。
安全问题同样值得关注。API令牌和Zone ID等敏感信息不应硬编码在脚本中,可以考虑将脚本权限设置为700,仅root可读写。如果多人使用同一台主机,更推荐将敏感值写入独立的环境文件,例如/etc/default/ddns-update,并在systemd服务中通过EnvironmentFile加载。此外,Cloudflare的API令牌支持IP白名单和过期时间,适当加以限制能进一步降低泄露风险。
TTL设置方面,如果IP变化频繁,可以将TTL调低到120秒甚至60秒,但过低的TTL会增加解析延迟和DNS查询负载。对于家庭宽带,通常120秒已经足够在IP变化后几分钟内完成同步。如果使用Cloudflare代理(proxied设为true),则面向公网的是Cloudflare的CDN IP,无法直接用于SSH等非HTTP协议,需要根据实际服务类型选择是否开启代理。