Cloudflare纯Bash动态DNS更新脚本:树莓派DDNS自动化方案
简介Cloudflare-DDNS-Update 是一款面向Linux/Unix系统运维人员、家庭服务器搭建者及中小型IT服务管理者的轻量级Bash动态DNS更新工具专为解决家庭宽带等动态IP环境下域名解析失效问题而设计。它通过调用Cloudflare官方API自动检测公网IPv4/IPv6地址并比对更新指定DNS区域内的A/AAAA记录支持定时任务集成、密钥安全认证与健壮错误处理适用于远程办公、个人博客托管及开发测试等需稳定域名访问的场景。资源包为5KB的ZIP压缩文件共含4个核心文件主执行脚本cf-update.sh实现DDNS逻辑、配置模板config-example.sh含API密钥与Zone ID等关键参数说明、CNAME记录示例文件辅助子域名映射及README.md含快速上手指南与使用说明。目前已有546人学习下载读者可直接部署运行获得开箱即用的Cloudflare动态DNS自动化能力、清晰的配置范式与生产就绪的脚本结构。1. Cloudflare-DDNS-Update 是什么一个能自动把家里树莓派的公网IP塞进Cloudflare DNS记录的 Bash 脚本专治“IP一变就打不开内网服务”的玄学崩溃你有没有过这种经历在家用树莓派搭了个 Home Assistant配好 Cloudflare CDN 做反代域名能访问、SSL 正常、页面加载飞快——结果某天凌晨三点手机弹窗告警“Home Assistant 不在线”。你抓狂登录路由器一看WAN 口 IP 地址变了。再冲进 Cloudflare 控制台手动改 A 记录等你点完保存监控已经连续告警 17 分钟。这不是运维事故是动态 IP 环境下最典型的“人肉 DDNS 疲劳综合征”。Cloudflare-DDNS-Update 就是为这个场景生的它不依赖任何第三方 DDNS 服务商不装 Python 包不跑 Docker 容器纯 Bash 实现5 分钟部署完就能在后台静默运行——只要你的机器能 curl、能读取/proc/sys/net/ipv4/conf/all/forwarding、能调用 Cloudflare API它就能每 5 分钟查一次当前 IPv4或 IPv6比对 DNS 记录只在真正需要时才发一次 PATCH 请求更新A或AAAA记录。它不是玩具脚本而是某实验室里三台边缘设备稳定运行 14 个月没出过一次 DNS 同步失败的真实生产级工具。适合所有用家用宽带、校园网、中小 ISP 线路做自建服务又不想被花生壳收费墙卡脖子的开发者、IoT 爱好者和 DevOps 初学者。2. 核心原理与选型依据为什么不用 inadyn、ddclient而坚持手写 Bash Cloudflare API v42.1 为什么拒绝传统 DDNS 客户端协议层兼容性黑洞与权限失控风险传统 DDNS 工具如ddclient或inadyn的设计哲学是“适配尽可能多的厂商”这导致它们必须内置大量硬编码的 URL 模板、认证方式HTTP Basic、URL 参数传 token、POST 表单、响应解析逻辑。但 Cloudflare API v4 是严格基于 RESTful Bearer Token JSON Schema 的现代接口它的PATCH /zones/{zone_id}/dns_records/{id}要求你精确提供content、name、type、proxied、ttl五项字段缺一不可而ddclient的 Cloudflare 插件早在 2021 年就因无法处理proxied: true场景下的 TTL 自动降级逻辑被社区标记为“半废弃”。更关键的是权限控制ddclient运行时需长期持有你的 Global API Key等同于账户最高权限一旦该进程被注入恶意代码攻击者可直接删光你所有 zone。而 Cloudflare-DDNS-Update 强制使用 scoped API Token最小权限仅需Zone:DNS:Edit且脚本本身不存储 token 到内存变量中全程通过curl -H Authorization: Bearer $TOKEN透传token 生命周期完全由系统环境变量或文件权限管控。2.2 为什么坚持纯 Bash启动快、无依赖、审计透明且足够表达复杂逻辑有人会问“Python 不是更适合 HTTP 请求和 JSON 解析吗”——确实但代价是你需要确保目标机器装了 Python 3.6、requests库、jq或自己写 JSON 解析器还要处理 SSL 证书验证路径尤其在嵌入式设备上/etc/ssl/certs/ca-certificates.crt位置不统一。而 Bash 的优势在于所有 Linux 发行版原生自带curl、jq、grep、sed、awk是事实标准Debian/Ubuntu 默认装curl和jqCentOS/RHEL 需yum install jq但仅此一项启动开销近乎为零ps aux | grep ddns看到的就是一个轻量进程不像 Python 进程要加载解释器和字节码逻辑完全暴露if [ $CURRENT_IP ! $RECORD_CONTENT ]; then ...这种判断比任何高级语言都直白审计时不用翻三层抽象就能确认“它到底有没有真的做对比再更新”。提示该脚本默认启用set -euo pipefail任何命令失败、未定义变量、管道错误都会立即退出避免“静默失败后 IP 滞后数小时”的黑匣子问题。2.3 关键数据流拆解从获取本机 IP 到触发 DNS 更新的七步闭环整个流程不依赖外部服务全部走本地探测 Cloudflare API本机 IPv4 探测优先读取ip -4 route get 8.8.8.8 | awk {print $7}绕过 NAT获取路由出口 IP失败则 fallback 到curl -s https://api.ipify.org经 Cloudflare CDN 加速的公开 IP 查询端点Zone ID 解析用curl -s -H Authorization: Bearer $TOKEN https://api.cloudflare.com/client/v4/zones?name$ZONE_NAME获取 zone 列表提取result[0].idDNS 记录查询curl -s -H Authorization: Bearer $TOKEN https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?typeAname$RECORD_NAME内容比对提取result[0].content与第 1 步获得的$CURRENT_IP字符串全等比较含空格、换行过滤变更决策仅当$CURRENT_IP ! $RECORD_CONTENT且RECORD_ID存在时进入更新分支安全更新构造 JSON payload{ content: $CURRENT_IP, name: $RECORD_NAME, type: A, proxied: $PROXIED, ttl: $TTL }执行PATCH状态落盘成功后将$CURRENT_IP写入/var/run/cloudflare-ddns.last供下次启动快速比对避免冷启动重复请求。这个闭环里没有“定时轮询”陷阱——它用sleep $INTERVAL控制频率但每次循环开头先kill -0 $PID_OF_PREV_INSTANCE检查是否已有实例在跑彻底杜绝 crontab 失控导致的并发更新冲突。3. 部署实操从下载到 systemd 服务化三步完成全自动值守3.1 下载与权限初始化不要直接 chmod x先校验再授权# 创建专用目录避免混入用户家目录造成权限混乱 sudo mkdir -p /opt/cloudflare-ddns cd /opt/cloudflare-ddns # 下载主脚本假设项目托管在 GitHub实际使用时替换为真实 release 地址 sudo curl -fsSL -o cloudflare-ddns.sh \ https://raw.githubusercontent.com/xxx/cloudflare-ddns/main/cloudflare-ddns.sh # 重点先检查脚本完整性项目若提供 SHA256SUMS 文件务必校验 # 若无校验文件则至少确认 shebang 行为 #!/usr/bin/env bash且无可疑 base64 块 sudo head -n 5 cloudflare-ddns.sh # 设置最小必要权限属主 root组 root仅 owner 可写 sudo chown root:root cloudflare-ddns.sh sudo chmod 755 cloudflare-ddns.sh逻辑说明chmod 755是因为脚本需被 systemd 以 root 身份执行且可能被其他管理脚本source设为700会导致systemd无法读取某些发行版 SELinux 策略限制755在保证可执行前提下让ls -l输出清晰可见权限结构。chown root:root是为后续日志写入/var/log/做准备——脚本内exec /var/log/cloudflare-ddns.log 21依赖此归属。3.2 配置文件生成用模板覆盖而非 sed 替换规避引号逃逸灾难# 创建配置目录分离代码与配置是生产环境铁律 sudo mkdir -p /etc/cloudflare-ddns # 生成配置文件注意TOKEN 必须用单引号包裹防止 shell 展开特殊字符 sudo tee /etc/cloudflare-ddns/config.env EOF # Cloudflare API Token最小权限Zone:DNS:Edit CF_API_TOKENyour_scoped_api_token_here # 目标 Zone 名称如 example.com不含 www CF_ZONE_NAMEexample.com # 待更新的 DNS 记录名称如 home.example.com CF_RECORD_NAMEhome.example.com # 是否开启 Cloudflare 代理true走 CDNfalse直连 CF_PROXIEDtrue # TTL 秒数1自动常见值300, 1200, 3600 CF_TTL1 # 检查间隔秒数建议 300~600太短触发 API 限频 CHECK_INTERVAL300 # 日志文件路径确保目录存在且可写 LOG_FILE/var/log/cloudflare-ddns.log EOF # 设置配置文件权限仅 root 可读写杜绝 token 泄露 sudo chown root:root /etc/cloudflare-ddns/config.env sudo chmod 600 /etc/cloudflare-ddns/config.env参数说明CF_PROXIEDtrue决定该记录是否显示橙色云朵图标。若你用 Cloudflare 做 SSL 终结和 WAF必须设为true若只是纯粹的 DNS 解析如指向内网 NAS 的 SMB 服务应设为false否则客户端会尝试连接 Cloudflare 边缘节点而非你的真实 IPCF_TTL1Cloudflare 特殊值表示“自动 TTL”实际生效值由 Cloudflare 根据proxied状态动态分配true时为 300 秒false时为 3600 秒比硬编码更健壮CHECK_INTERVAL300官方文档明确写出 API 限频为 1200 次/5 分钟按单次更新耗 2 次请求GET zone GET record PATCH record 3 次计算300 秒间隔可支撑最多 4 个不同记录的轮询留足余量。3.3 systemd 服务注册带健康检查的守护进程不是简单的一行 crontab# 创建 systemd service 文件 sudo tee /etc/systemd/system/cloudflare-ddns.service EOF [Unit] DescriptionCloudflare Dynamic DNS Updater Documentationhttps://github.com/xxx/cloudflare-ddns Afternetwork-online.target Wantsnetwork-online.target [Service] Typesimple Userroot EnvironmentFile/etc/cloudflare-ddns/config.env ExecStart/opt/cloudflare-ddns/cloudflare-ddns.sh Restarton-failure RestartSec30 StartLimitInterval600 StartLimitBurst5 StandardOutputjournal StandardErrorjournal SyslogIdentifiercloudflare-ddns # 防止 OOM killer 杀死进程小内存设备必备 MemoryLimit50M [Install] WantedBymulti-user.target EOF # 重载 systemd 配置并启用服务 sudo systemctl daemon-reload sudo systemctl enable --now cloudflare-ddns.service # 验证服务状态重点关注 Active: active (running) 和 Latest log sudo systemctl status cloudflare-ddns.service逻辑说明Typesimple表明脚本自身包含无限循环逻辑无需 systemd 帮忙拉起多次Restarton-failure配合RestartSec30实现故障自愈但StartLimit*参数防止崩溃风暴——10 分钟内最多重启 5 次超限则停服并报错逼你去查日志MemoryLimit50M是针对树莓派 2GB 内存设备的保守值实测该脚本常驻内存仅 3~5MB设 50M 是为curl和jq的临时缓冲区留足空间避免fork: Cannot allocate memory错误。4. 避坑指南五个血泪经验总结的高频翻车点与根因修复4.1 现象脚本首次运行报错{success:false,errors:[{code:6003,message:Invalid request headers}]}原因API Token 权限不足或格式错误。Cloudflare 的 scoped Token 必须显式勾选Zone:DNS:Edit且不能混用 Global API Key以v1.开头Token 字符串若含空格或换行复制时误触curl会将其作为多个参数传递导致 header 解析失败。解决进入 Cloudflare Dashboard → My Profile → API Tokens → Create Token → 选择 “Edit zone DNS” 模板 → 在 Permissions 中确认Zone→DNS→Edit为Granted→ 在 Zone Resources 中选择具体 zone → 创建后用echo $CF_API_TOKEN | wc -c检查长度是否异常正常应为 40 字符并用cat /etc/cloudflare-ddns/config.env | grep CF_API_TOKEN确认无前后空格。4.2 现象日志显示Current IP: 192.168.1.100但 DNS 记录始终不更新原因脚本默认探测的是本机路由出口 IP但在 NAT 环境下如光猫桥接软路由ip route get 8.8.8.8返回的是软路由 LAN 口 IP192.168.x.x而非公网 IP。这是最隐蔽的“假成功”陷阱。解决强制使用公网探测模式。编辑/etc/cloudflare-ddns/config.env添加一行CF_IP_METHODpublic然后在脚本中搜索get_current_ip()函数将elif [ $CF_IP_METHOD public ]; then分支取消注释并确保curl -s https://api.ipify.org可达测试curl -v https://api.ipify.org 21 | grep HTTP/2 200。4.3 现象systemd 服务状态为active (exited)日志提示No such file or directory原因ExecStart路径错误或脚本第一行 shebang 指向不存在的解释器。常见于从 Windows 编辑后上传的脚本其换行符为 CRLF导致#!/usr/bin/env bash^M被识别为非法命令。解决用file /opt/cloudflare-ddns/cloudflare-ddns.sh检查文件格式若输出含CRLF则执行sudo sed -i s/\r$// /opt/cloudflare-ddns/cloudflare-ddns.sh再用head -n1 /opt/cloudflare-ddns/cloudflare-ddns.sh确认首行为#!/usr/bin/env bash无 ^M最后sudo systemctl daemon-reload sudo systemctl restart cloudflare-ddns.service。4.4 现象DNS 记录内容更新了但proxied状态被重置为false原因脚本在PATCH请求中未显式传递proxied字段。Cloudflare API 规范要求PATCH 操作只更新 payload 中出现的字段未出现的字段保持原值但某些旧版脚本在构造 JSON 时漏掉了proxied: $CF_PROXIED。解决打开脚本定位到update_dns_record()函数内的payload部分确认 JSON 字符串包含proxied: ${CF_PROXIED}注意$CF_PROXIED是字符串需用${CF_PROXIED}防止空值扩展若使用jq构造应为jq -n --arg ip $CURRENT_IP --arg proxied $CF_PROXIED {content: $ip, name: $RECORD_NAME, type: A, proxied: ($proxied | test(true)), ttl: $CF_TTL}。4.5 现象systemctl status显示Failed with result exit-code日志末尾是jq: error (at stdin:1): Cannot index array with string result原因Cloudflare API 返回空结果或格式变更。典型场景是CF_ZONE_NAME配置错误如填了www.example.com而非example.com导致zones?name...查询返回{result:[],success:true,errors:[],messages:[]}jq .result[0].id对空数组取[0]报错。解决手动执行查询命令调试curl -s -H Authorization: Bearer $CF_API_TOKEN https://api.cloudflare.com/client/v4/zones?name$CF_ZONE_NAME检查result数组长度若为空登录 Cloudflare 确认该域名已正确添加到账户且状态为Active脚本中应增加if [ $(echo $ZONE_RESP | jq .result | length) -eq 0 ]; then echo ERROR: Zone $CF_ZONE_NAME not found; exit 1; fi做前置校验。5. 进阶技巧如何验证 DNS 更新真实性、实现多记录同步、以及给脚本加“后悔药”5.1 DNS 更新真实性验证三重交叉校验法拒绝“我以为它好了”光看systemd日志说Updated A record for home.example.com to 203.0.113.42不够必须验证三个层面API 层面curl -s -H Authorization: Bearer $TOKEN https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$RECORD_ID | jq .result.content,.result.proxied,.result.ttl—— 确认返回的content等于你期望的 IPproxied等于配置值ttl符合预期DNS 解析层面dig short home.example.com 1.1.1.1用 Cloudflare 公共 DNS和dig short home.example.com 8.8.8.8用 Google DNS—— 两者结果必须一致且等于新 IP排除本地 DNS 缓存干扰网络可达层面curl -I http://home.example.com --connect-timeout 5—— 若返回HTTP/2 200或HTTP/1.1 200证明 DNS 解析 网络路径 服务监听全部打通。提示把这三步写成verify-ddns.sh脚本放在/usr/local/bin/下每次更新后手动跑一次形成肌肉记忆。5.2 多 DNS 记录同步用数组驱动避免复制粘贴式维护灾难原脚本默认只更新单条记录但生产环境常需同步home.example.comA 记录、api.example.comA 记录、blog.example.comAAAA 记录三条。不推荐复制三份脚本而应改造为数组驱动# 在 /etc/cloudflare-ddns/config.env 中定义多记录 CF_RECORDS( home.example.com:A:true api.example.com:A:false blog.example.com:AAAA:true ) # 修改脚本中的 main() 循环 for record_spec in ${CF_RECORDS[]}; do IFS: read -r RECORD_NAME RECORD_TYPE PROXIED $record_spec # 后续调用 get_zone_id、get_dns_record、update_dns_record 时传入这三个变量 done参数说明IFS:将冒号设为分隔符read -r逐字段赋值-r防止反斜杠转义RECORD_TYPE支持A/AAAA脚本中需相应调整curl查询参数?type$RECORD_TYPE和 payload 构造PROXIED字符串需转换为布尔值[ $PROXIED true ] PROXIED_BOOLtrue || PROXIED_BOOLfalse。5.3 “后悔药”机制自动备份 DNS 记录变更历史支持一键回滚DNS 更新不可逆一旦误操作可能导致服务中断。脚本应自带变更审计能力# 在 update_dns_record() 成功后追加备份逻辑 backup_file/var/backups/cloudflare-ddns/$(date %Y%m%d_%H%M%S)_${RECORD_NAME}.json mkdir -p $(dirname $backup_file) # 保存更新前的完整 record 对象从 GET 响应中提取 echo $RECORD_RESP $backup_file # 同时记录操作者如果是手动触发和 IP 来源 echo Updated by: $(whoami) | Source IP: $CURRENT_IP | At: $(date) $backup_file # 回滚脚本 rollback-last.sh 示例 # 1. 读取最新备份文件名ls -t /var/backups/... | head -1 # 2. 提取其中的 content 字段jq -r .content # 3. 调用 PATCH 接口写回该 content从那以后我每次上线新脚本都强制走一遍curl -s https://api.cloudflare.com/client/v4/zones?namexxx | jq .result[].name确认 zone 存在再dig short xxx 1.1.1.1确认当前解析值最后才执行systemctl start。这三步 20 秒的事省去半夜爬起来修 DNS 的 2 小时。希望帮到你。本文还有配套的精品资源点击获取