适用场景:软路由跑 RouterOS 7.15,PPPoE 拨号上网,域名解析用 Cloudflare 管理,需要动态更新公网 IP。
一、整体思路
路由器定时执行一个脚本:直接从 PPPoE 拨号接口读取当前公网 IP → 查询 Cloudflare 上该域名当前解析的 IP → 两者不一致就调用 Cloudflare API 更新。
二、确认 WAN 拨号接口名称
终端执行:
/interface pppoe-client print
输出里 name 那一列就是接口名(常见是 pppoe-out1),记下来,后面脚本要用。
三、Cloudflare 侧准备
打开 https://dash.cloudflare.com → 右上角头像 → My Profile → API Tokens → Create Token
选择 Create Custom Token:
Permissions:
Zone-DNS-EditZone Resources:
Specific zone,选中你要更新的域名(不要选全部域名,权限最小化)
创建后复制生成的 Token(只显示一次,务必先存好)
打开该域名的 Cloudflare Dashboard 首页,右侧栏 API 区域,复制 Zone ID
在 Cloudflare 上手动先建好一条 A 记录(比如
home.dragonwebs.vicp.net),IP 随便填一个占位值(如1.1.1.1),脚本会自动把它改成真实 IP
四、导入 CA 根证书(解决 HTTPS 证书校验问题)
RouterOS 默认对 HTTPS 请求做证书链校验,不导入证书会直接报错 SSL: no trusted CA certificate found。
1)下载完整的 CA 证书包(Mozilla 官方信任列表):
/tool fetch url="https://curl.se/ca/cacert.pem" dst-path=cacert-full.pem check-certificate=no
(这一步用 check-certificate=no 是因为此刻信任链还没建立,属于先有鸡还是先有蛋的特例,仅用于首次获取证书文件)
2)确认下载成功:
/file print where name~"cacert-full"
正常应显示一个约 180KB 左右的 .pem 文件。
3)导入证书:
/certificate import file-name=cacert-full.pem
提示 passphrase 时直接回车。正常返回类似:
certificates-imported: 120
(一百多个根证书就说明成功,涵盖 DigiCert、ISRG/Let's Encrypt、Amazon、Google 等主流签发机构)
4)确认证书库:
/certificate print
能看到一长串证书列表,T 标记代表已信任即可。
五、创建 DDNS 脚本
用户配置区(部署时必须修改的 5 项):
:local wanInterface "pppoe-out1"
:local cfToken "你的CloudflareAPIToken"
:local cfZoneID "你的ZoneID"
:local cfRecordName "home.dragonwebs.vicp.net"
:local checkCert "yes-without-crl"
完整脚本正文:
:local wanInterface "pppoe-out1"
:local cfToken "你的CloudflareAPIToken"
:local cfZoneID "你的ZoneID"
:local cfRecordName "home.dragonwebs.vicp.net"
:local checkCert "yes-without-crl"
# ========== 用户配置区结束 ==========
:log info "cf-ddns: 开始检测"
# ---- 1. 直接从WAN接口读取当前公网IP ----
:local dipaddr ""
:do {
:local rawAddr [/ip address get [/ip address find interface=$wanInterface] address]
:local slashPos [:find $rawAddr "/"]
:set dipaddr [:pick $rawAddr 0 $slashPos]
} on-error={
:log error ("cf-ddns: 未能从接口 " . $wanInterface . " 读取到地址,请检查接口名称/拨号是否成功")
}
:if ([:len $dipaddr] = 0) do={
:error "cf-ddns: dipaddr为空,脚本终止"
}
:log info ("cf-ddns: 当前WAN口公网IP = " . $dipaddr)
# ---- 2. 查询 Cloudflare 上该记录当前的值 ----
:local cfQueryUrl ("https://api.cloudflare.com/client/v4/zones/" . $cfZoneID . "/dns_records?type=A&name=" . $cfRecordName)
:local cfData ""
:do {
:local cfResult [/tool fetch url=$cfQueryUrl http-method=get \
http-header-field=("Authorization: Bearer " . $cfToken . ",Content-Type: application/json") \
output=user as-value check-certificate=$checkCert]
:set cfData ($cfResult->"data")
} on-error={
:log error "cf-ddns: 查询Cloudflare记录失败(网络/证书/Token问题),脚本终止"
}
:if ([:len $cfData] = 0) do={
:error "cf-ddns: Cloudflare返回数据为空,退出"
}
# ---- 3. 从返回JSON里提取 record id 和 当前 content(IP) ----
:local idPos [:find $cfData "\"id\":\""]
:if ([:typeof $idPos] = "nil") do={
:error "cf-ddns: 未找到记录ID。请确认: 1)域名拼写与Cloudflare上完全一致 2)Cloudflare上已存在这条A记录 3)ZoneID正确"
}
:local idStart ($idPos + 6)
:local idEnd [:find $cfData "\"" $idStart]
:local recordID [:pick $cfData $idStart $idEnd]
:local contentPos [:find $cfData "\"content\":\""]
:local contentStart ($contentPos + 11)
:local contentEnd [:find $cfData "\"" $contentStart]
:local currentDNS [:pick $cfData $contentStart $contentEnd]
:log info ("cf-ddns: Cloudflare当前解析IP = " . $currentDNS . " , recordID = " . $recordID)
# ---- 4. 比较并按需更新 ----
:if ($dipaddr = $currentDNS) do={
:log info "cf-ddns: IP未变化,无需更新"
} else={
:log info "cf-ddns: 检测到IP变化,开始更新Cloudflare记录"
:local patchUrl ("https://api.cloudflare.com/client/v4/zones/" . $cfZoneID . "/dns_records/" . $recordID)
:local patchData ("{\"type\":\"A\",\"name\":\"" . $cfRecordName . "\",\"content\":\"" . $dipaddr . "\",\"ttl\":120,\"proxied\":false}")
:do {
/tool fetch url=$patchUrl http-method=patch \
http-header-field=("Authorization: Bearer " . $cfToken . ",Content-Type: application/json") \
http-data=$patchData output=none check-certificate=$checkCert
:log info ("cf-ddns: 更新成功! " . $cfRecordName . " -> " . $dipaddr)
} on-error={
:log error "cf-ddns: 更新Cloudflare记录失败,请检查Token权限"
}
}
创建为 script 对象(WinBox 图形方式):
System → Scripts → 加号(+),Name 填 cf-ddns,Policy 勾选 read, write, test, ftp,把上面脚本正文粘贴进 Source 框,点 OK。
或者用终端命令:
/system script add name=cf-ddns policy=read,write,test,ftp source={
<脚本正文粘贴在这里>
}
六、手动测试
/system script run cf-ddns
/log print where message~"cf-ddns"
正常日志顺序:
cf-ddns: 开始检测
cf-ddns: 当前WAN口公网IP = xxx.xxx.xxx.xxx
cf-ddns: Cloudflare当前解析IP = ... , recordID = ...
cf-ddns: 检测到IP变化,开始更新Cloudflare记录
cf-ddns: 更新成功! xxx -> xxx.xxx.xxx.xxx
第一次跑因为占位 IP 和真实 IP 不同,一定会触发更新,去 Cloudflare Dashboard 刷新确认 A 记录已变成真实 IP。再跑一次应显示:
cf-ddns: IP未变化,无需更新
说明比较逻辑正常。
七、设置定时任务
/system scheduler add name=cf-ddns-timer interval=5m start-time=startup on-event="/system script run cf-ddns"
确认已添加:
/system scheduler print
interval=5m 即每 5 分钟检查一次,可按需调整为 10m、30m,Cloudflare API 限额很宽松。
八、故障排查表
九、日常维护
每隔几天执行一次
/log print where message~"cf-ddns"抽查日志,确认没有反复报错如果一开始用
checkCert="no"跑通的,之后有空可以重新走一遍第四步导入更完整的证书包,再切回"yes-without-crl",不影响当前功能正常使用Token 建议定期(比如半年到一年)在 Cloudflare 后台轮换一次,安全性更好
评论区