2025年8月

刚注意到我的一台路由器的 DDNS 功能失灵了,看了一下用的是一个从 github 上找的支持 cloudflare DNS 的脚本,现在一执行就报错。本想改改继续用,觉得这个脚本不够优雅,还要生成临时文件。于是就自己写了一个,特点是支持多路 PPPOE 拨号,不生成临时文件,也不定义临时的全局变量,并支持发送钉钉消息(如不需要把 dingtalk 值留空或相关语句屏蔽即可)。

脚本实际上分为两个,第一个脚本放在 /ppp/Profiles 那里,打开 pppoe 拨号使用的那个 profile 条目,在 scripts 标签页下的 On Up 编辑框里填写如下内容:

# 定义要调用的 DDNS 脚本名称
:local scrName "CloudFlare_DDNS"
# 获取拨号接口名称
:local ifname [/interface get $interface name];
# 获取公网IP
:local currentIP $"local-address";
# 定义要传输的变量
:local var1 "ifname=$ifname"
:local var2 "currentIP=$currentIP"
# 调用 DDNS 脚本并注入变量
:execute script="[[:parse \"[:parse [/system script get $scrName source]] $var1 $var2\"]]"

第二个脚本在 /system/scripts 处添加,命名为“CloudFlare_DDNS”,内容如下:

# =========================================================
# --- Cloudflare DDNS 全自动脚本 for RouterOS v7 ---
# --- 自动从On Up事件获取IP和接口 ---
# =========================================================

# ----------------------------------------------------
# --- 参数配置 ---
# ----------------------------------------------------
# CloudFlare Token(DNS 权限)
:local cfToken  "你的 Token 数据"
# CloudFlare 区域 ID
:local cfZoneId "你的区域 ID"

# --- 接口 -> 域名 映射关系 (在这里扩展) ---
# 如果只有一条拨号线路,只须将域名赋值给 cfDomain,将运营商名字赋值给 ISP;
# 后面两行 if 语句屏蔽即可。
:local cfDomain "lt.yourdomain.com"
:local ISP "联通"
# 多条线路时使用
# 如果有多条拨号线路,上面的 cfDomain 和 ISP 两个值留空(不可删除);
# 取消下面两条 if 语句前面的 #,并按实际情况填写 ifname(拨号接口名称)、cfDomain 和 ISP。
# if ($ifname = "pppoe-out-LT") do={:set cfDomain "lt.yourdomain.com";:set ISP "联通"}
# if ($ifname = "pppoe-out-YD") do={:set cfDomain "yd.yourdomain.com";:set ISP "移动"}
# 如果有第三个拨号,在这里继续添加。

# --- 调用钉钉发送通知(可选,关键字方式)---
# 不需要钉钉时可以将 url 变量留空,或者全部删除
:global dingtalk do={
    :local keyword "关键字";
    :local url  "https://oapi.dingtalk.com/robot/send?access_token=你的钉钉token"
    :local now ([/system/clock get date] ." ".[/system/clock get time])
    :local data "{\"msgtype\": \"text\", \"text\": {\"content\": \"$now $1 $keyword\"}}"
    :local header "Content-Type:application/json";
    :log info "data: $data"
     if ([:len $url] > 0) do={
    do {  
        /tool/fetch http-method=post mode=https http-header-field="$header" http-data="$data" url="$url"
    } on-error={log error "发送钉钉消息失败。"}
    }
}

# ----------------------------------------------------
# --- 合法性检查 ---
# 接口检查
if ([:len $ifname] = 0) do={
    :log warning "未获取到 PPPOE 接口名称,脚本退出。"
    :quit
}
# 域名检查
if ([:len $cfDomain] = 0) do={
    :log warning "域名为空,脚本退出。"
    :quit
}
# 公网 IP 检查
if ([:len $currentIP] = 0) do={
    :log warning "未获取到公网 IP 地址,脚本退出。"
    :quit
} else={:log info "$ISP 接口名称:$ifname,公网 IP 地址:$currentIP,匹配域名:$cfDomain"}

# ---  查询Cloudflare上的DNS记录信息 ---
:local recordId ""
:local dnsIP ""
:local recordQueryUrl "https://api.cloudflare.com/client/v4/zones/$cfZoneId/dns_records?name=$cfDomain&type=A"
:local queryResponse [/tool fetch url=$recordQueryUrl http-header-field="Authorization: Bearer $cfToken" as-value output=user]
if ([:find ($queryResponse->"data") "\"success\":true"] != nil) do={
    :local responseData ($queryResponse->"data")
    :local idStart [:find $responseData "\"id\":\""]
    if ($idStart != nil) do={
        :set idStart ($idStart + 6); :local idEnd [:find $responseData "\"" $idStart]; :set recordId [:pick $responseData $idStart $idEnd]
        :local contentStart ([:find $responseData "\"content\":\""] + 11); :local contentEnd [:find $responseData "\"" $contentStart]; :set dnsIP [:pick $responseData $contentStart $contentEnd]
        :log info "$ISP接口:域名 $cfDomain 当前解析记录为: $dnsIP"
    } else={ :log error "$ISP接口:API调用成功,但未找到域名 $cfDomain 的 A 类型记录。"; :error "未找到DNS记录" }
} else={ :log error "$ISP接口:查询 DNS 信息失败。"; :error "查询DNS信息失败" }

# --- 对比IP并决定是否更新 ---
if ([:len $recordId] > 0) do={
    if ($currentIP = $dnsIP) do={
        :log info "$ISP接口:IP地址 ($currentIP) 未变更,无需更新。"
    } else={
        :log warning "$ISP接口:IP地址已变更 ($dnsIP -> $currentIP)。准备更新..."
        # --- 执行更新操作 ---
        :local cfApiUrl "https://api.cloudflare.com/client/v4/zones/$cfZoneId/dns_records/$recordId"
        :local jsonData "{\"type\":\"A\",\"name\":\"$cfDomain\",\"content\":\"$currentIP\",\"ttl\":120,\"proxied\":false}"
        :local updateResponse [/tool fetch url=$cfApiUrl http-method=put http-header-field="Authorization: Bearer $cfToken,Content-Type: application/json" http-data=$jsonData as-value output=user]
        if ([:find ($updateResponse->"data") "\"success\":true"] != nil) do={
            :local msg "$ISP接口:公网 IP 变更为  $currentIP,域名 $cfDomain 的DNS记录更新成功。"
            :log info $msg
            # ---发送钉钉消息,可选 ---
            [$dingtalk $msg]
        } else={ :log error "$ISP接口:DNS记录更新失败。" }
    }
}

CloudFlare 的 Token 和 ZoneId 数据需要自行在 CloudFlare 官网上去设置或查找。
和其它脚本需要手工指定拨号接口、从 /ip/address 处查询 IP、甚至需要生成全局变量或临时文件不同,这个脚本的有趣之处在利用 On Up 事件时系统返回的端口 ID 号和 IP 地址,并巧妙地传递给了修改域名解析的脚本(RouterOS 脚本并不支持命令行方式传入参数)。

前言:
RouterOS 路由器单纯的使用脚本无法实现阿里云动态域名功能(无法给请求数据加签),需要借助其它代码写的转发代理。以前是在一个 Windows 版的工具里增加了相关功能代码,后来服务器要关掉,准备找个能部署到网站上的版本。网上找了一圈发现很多代码都是基于阿里云 SDK 的,需要在服务器上安装,而我用的那个网站空间只能上传文件不支持安装。于是自己写了一个基于阿里云 OpenAPI V3 版签名机制的 PHP 版本,只有一个 PHP 文件和一个 RouterOS 脚本,实测使用正常。顺带感慨:AI 辅助编程真的很方便。

RouterOSv7 For Aliyun DDNS
支持阿里云域名动态解析的 RouterOS V7 脚本及对应的转发代理。

说明:

  • 需要 RouterOS 脚本搭配一个转发代理程序使用。转发代理使用阿里云的 V3 版签名方案,只有一个 PHP 文件,不需要安装 SDK,可部署到各种 PHP 网站空间。
  • 只支持 RouterOS V7 以上版本。转发代理功能经过简化,只支持以 GET 方式进行域名查询和修改两个功能。
  • RouterOS 的开头部分为用户变量,需要自行修改,详见注释。
  • 域名的 DNS 服务器要指向阿里云。需要在阿里云上自行配置一个支持 DNS 解析权限的 AccessKey。
  • 为防止转发代理被滥用,使用了安全令牌(securityToken)和白名单,安全令牌需要 RouterOS 脚本和 PHP 代码中保持一致,白名单在 PHP 代码中添加。详见代码中的注释。
  • RouterOS 脚本中增加了一个通过钉钉机器人发送消息通知功能,若不需要可自行屏蔽。
  • RouterOS 脚本目前的设置是 IPV4,如要支持 IPV6,需要将 recordType 的值由 A 改为 AAAA,同时修改获取公网 IP 的方式:如从支持 IPV6 的路由器接口获取 IP,或者由 https://6.ipw.cn 之类的网站返回 IP。
  • 转发代理目前只有一个 PHP 版本,以后可能会增加其它语言版本。
  • PHP 端有日志功能,并可自行关闭,详见代码中用户配置部分。日志中不会记录用户的 AccessKeySecret。
  • RouterOS 脚本建议由 PPPOE 拨号 的 Profiles 配置中的 On Up 脚本中调用。最好是先用 delay 3s 命令延时几秒。
  • RouterDDNS.txt 为 RouterOS 脚本文件。
  • aliyun-ddns.php 为 PHP 转发代理。

下载:RouterOSv7-Aliyun-DDNS.zip

https://github.com/SilenceCCF/RouterOSv7-Aliyun-DDNS