概述
动态DNS(DDNS)是网络管理中的重要技术,特别是对于使用动态IP地址的家庭或小型企业网络。当需要管理多个域名解析记录时,手动更新变得繁琐且容易出错。本文将详细介绍如何在RouterOS设备上实现批量DDNS更新脚本,支持CloudFlare DNS服务,并能够保持原有的CDN代理设置。
技术原理
DDNS工作机制
DDNS的核心工作原理:
- IP监测:定期检查WAN接口的公网IP地址
- 变化检测:与之前记录的IP地址进行比较
- 批量更新:当IP发生变化时,批量更新所有相关域名记录
- 状态保持:保持CloudFlare CDN代理设置不变
CloudFlare API集成
脚本通过CloudFlare API v4实现DNS记录管理:
- 认证方式:使用Bearer Token进行API认证
- 操作权限:需要Zone:Read和Zone:Edit权限
- 批量处理:单次脚本执行可更新多个域名记录
- 状态保持:读取并保持每个记录的代理状态
脚本功能特性
核心功能
- 自动IP检测:支持PPPoE、静态IP等多种WAN接口类型
- 批量域名管理:一次性管理多个子域名记录
- 代理状态保持:保持CloudFlare CDN代理设置不变
- 错误恢复:初始化时自动创建配置文件
- 调试支持:可开启详细的调试日志
安全特性
- API密钥保护:使用环境变量或安全配置存储密钥
- 权限最小化:仅请求必要的DNS操作权限
- 请求限制:内置API调用间隔,避免触发频率限制
完整配置模板
################# CloudFlare 批量DDNS脚本 (保持CDN设置版本) #################
# 是否开启debug调试模式
:local CFDebug "false"
# 是否开启CFcloud功能(使用RouterOS云服务获取公网IP)
:local CFcloud "false"
# 修改为有公网IP的接口名称(例如:pppoe、ether1等)
:global WANInterface "pppoe"
# CloudFlare 全局密钥token或者有权限操作解析域名的token
:local CFtkn "your_cloudflare_token_here"
# 域名zoneId(在CloudFlare控制台右侧栏可以找到)
:local CFzoneid "your_zone_id_here"
# 记录类型 一般无需修改
:local CFrecordType "A"
# 记录ttl值,一般无需修改
:local CFrecordTTL "120"
# 定义所有需要更新的域名和对应的记录ID
# 格式:"域名"="记录ID"
:local domainList {
"api.example.com"="record_id_1";
"blog.example.com"="record_id_2";
"chat.example.com"="record_id_3";
"home.example.com"="record_id_4"
}
:log info "开始批量更新解析记录..."
################# 内部变量 variables #################
:local previousIP ""
:global WANip ""
################# 获取DNS记录当前代理状态的函数 #################
:local getCurrentProxyStatus do={
:local recordId $1;
:local zoneId $2;
:local token $3;
:local debug $4;
:local CFurl "https://api.cloudflare.com/client/v4/zones/$zoneId/dns_records/$recordId";
:local tmpFile "cf-record-$recordId.tmp";
:do {
/tool fetch http-method=get mode=https url="$CFurl" http-header-field="Authorization:Bearer $token" dst-path="$tmpFile";
:delay 1s;
:if ([/file find name=$tmpFile] != "") do={
:local content [/file get [/file find name="$tmpFile"] contents];
/file remove $tmpFile;
# 简单解析JSON,查找proxied字段
:local proxiedPos [:find $content "\"proxied\":"];
:if ($proxiedPos != "") do={
:local valueStart ($proxiedPos + 10);
:local valueEnd [:find $content "," $valueStart];
:if ($valueEnd = "") do={ :set valueEnd [:find $content "}" $valueStart]; }
:local proxiedValue [:pick $content $valueStart $valueEnd];
:if ($debug = "true") do={
:log info ("CF: 记录 $recordId 当前代理状态: $proxiedValue");
}
:if ($proxiedValue = "true") do={
:return "true";
} else={
:return "false";
}
}
}
} on-error={
:if ($debug = "true") do={
:log error ("CF: 获取记录 $recordId 代理状态失败,默认使用false");
}
:return "false";
}
:return "false";
}
################# 获取或设置以前的ip变量 #################
:if ($CFcloud = "true") do={
:set WANip [/ip cloud get public-address]
};
:if ($CFcloud = "false") do={
:local currentIP [/ip address get [/ip address find interface=$WANInterface ] address];
:set WANip [:pick $currentIP 0 [:find $currentIP "/"]];
};
# 定义统一的临时文件名
:local tmpFileName "batch-ddns-ip.txt"
# 检查并创建临时文件
:if ([/file find name=$tmpFileName] = "") do={
:log error "没有找到记录前一个公网IP地址的文件, 自动创建..."
:set previousIP $WANip;
:execute script=":put $WANip" file=[:pick $tmpFileName 0 [:find $tmpFileName "."]];
:delay 2s;
:log info ("CF: 初始化批量DDNS, 当前IP = $WANip")
# 批量更新所有域名(初始化时保持代理状态)
:foreach domain,recordId in=$domainList do={
:local CFurl "https://api.cloudflare.com/client/v4/zones/$CFzoneid/dns_records/$recordId";
:local proxied [$getCurrentProxyStatus $recordId $CFzoneid $CFtkn $CFDebug];
:log info ("CF: 初始化更新域名 $domain = $WANip (代理: $proxied)")
/tool fetch http-method=put mode=https output=none url="$CFurl" http-header-field="Authorization:Bearer $CFtkn,content-type:application/json" http-data="{\"type\":\"$CFrecordType\",\"name\":\"$domain\",\"ttl\":$CFrecordTTL,\"content\":\"$WANip\",\"proxied\":$proxied}"
:delay 1s;
}
:log info "初始化完成,已更新所有域名记录,脚本将继续执行正常检查"
} else={
:if ( [/file get [/file find name=$tmpFileName] size] > 0 ) do={
:global content [/file get [/file find name=$tmpFileName] contents] ;
:global contentLen [ :len $content ] ;
:global lineEnd 0;
:global line "";
:global lastEnd 0;
:set lineEnd [:find $content "\n" $lastEnd ] ;
:set line [:pick $content $lastEnd $lineEnd] ;
:set lastEnd ( $lineEnd + 1 ) ;
:if ( [:pick $line 0 1] != "#" ) do={
:set previousIP [:pick $line 0 $lineEnd ];
# 清理所有可能的空白字符
:if ([:find $previousIP "\r"] != "") do={
:set previousIP [:pick $previousIP 0 [:find $previousIP "\r"]];
}
:if ([:find $previousIP "\n"] != "") do={
:set previousIP [:pick $previousIP 0 [:find $previousIP "\n"]];
}
# 去除前后空格
:while ([:pick $previousIP 0 1] = " ") do={
:set previousIP [:pick $previousIP 1 [:len $previousIP]];
}
:while ([:pick $previousIP ([:len $previousIP] - 1) [:len $previousIP]] = " ") do={
:set previousIP [:pick $previousIP 0 ([:len $previousIP] - 1)];
}
}
} else={
:log warning ("CF: 临时文件 $tmpFileName 为空或不存在,将当前IP设为前一个IP")
:set previousIP ""
}
}
######## 将调试信息写入日志 #################
:log info ("CF: 前一个解析IP地址 = '$previousIP' (长度: " . [:len $previousIP] . ")")
:log info ("CF: 当前WAN接口IP = '$WANip' (长度: " . [:len $WANip] . ")")
:log info ("CF: 需要更新的域名数量 = " . [:len $domainList])
:if ($CFDebug = "true") do={
:log info ("CF: IP比较详情 - 前一个IP长度: " . [:len $previousIP] . ", 当前IP长度: " . [:len $WANip])
:log info ("CF: 前一个IP转hex: " . [:tostring $previousIP mode=hex])
:log info ("CF: 当前IP转hex: " . [:tostring $WANip mode=hex])
};
######## 比较并批量更新记录 #####
:if ($previousIP != $WANip) do={
:log info ("CF: IP地址已变更,开始批量更新解析记录: $previousIP -> $WANip")
:local updateCount 0;
:local successCount 0;
:local failCount 0;
# 批量更新所有域名(保持代理状态)
:foreach domain,recordId in=$domainList do={
:set updateCount ($updateCount + 1);
:local CFurl "https://api.cloudflare.com/client/v4/zones/$CFzoneid/dns_records/$recordId";
# 获取当前记录的代理状态
:local proxied [$getCurrentProxyStatus $recordId $CFzoneid $CFtkn $CFDebug];
:log info ("CF: [$updateCount/" . [:len $domainList] . "] 更新域名 $domain = $WANip (保持代理: $proxied)")
:do {
/tool fetch http-method=put mode=https url="$CFurl" http-header-field="Authorization:Bearer $CFtkn,content-type:application/json" output=none http-data="{\"type\":\"$CFrecordType\",\"name\":\"$domain\",\"ttl\":$CFrecordTTL,\"content\":\"$WANip\",\"proxied\":$proxied}"
:set successCount ($successCount + 1);
:log info ("CF: 成功更新域名 $domain (代理状态: $proxied)")
} on-error={
:set failCount ($failCount + 1);
:log error ("CF: 更新域名 $domain 失败")
}
# 添加延迟避免API限制
:delay 2s;
}
:log info ("CF: 批量更新完成! 成功: $successCount, 失败: $failCount, 总数: $updateCount")
# 清空DNS缓存
/ip dns cache flush
# 更新临时文件中的IP记录
:if ( [/file get [/file find name=$tmpFileName] size] > 0 ) do={
/file remove $tmpFileName
}
:execute script=":put $WANip" file=[:pick $tmpFileName 0 [:find $tmpFileName "."]]
} else={
:log info "CF: IP地址未发生改变,无需更新! 当前IP: $WANip"
}
:log info "批量DDNS脚本执行完成."
配置步骤详解
步骤1:获取CloudFlare凭据
- 登录CloudFlare控制台
- 访问 CloudFlare Dashboard
- 选择需要管理的域名
- 获取Zone ID
- 在域名概览页面右侧栏找到"Zone ID"
- 复制并替换脚本中的
CFzoneid
变量
- 创建API Token
- 访问 API Tokens页面
- 点击"Create Token"
- 选择"Custom token"模板
- 设置权限:
- Zone:Zone:Read
- Zone:DNS:Edit
- 设置Zone Resources:选择对应的域名
- 复制生成的Token替换
CFtkn
变量
步骤2:获取DNS记录ID
使用以下API调用获取域名的记录ID:
# 替换YOUR_TOKEN和YOUR_ZONE_ID
curl -X GET "https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/dns_records" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json"
从返回的JSON中找到对应域名的 id
字段,更新脚本中的 domainList
。
步骤3:RouterOS配置
- 上传脚本
# 通过WinBox或命令行上传脚本文件
/system script add name="batch-ddns" source=[/file get batch-ddns.script contents]
- 配置定时任务
# 每5分钟执行一次检查
/system scheduler add name="ddns-check" interval=00:05:00 on-event="/system script run batch-ddns"
- 验证WAN接口名称
# 查看接口列表
/interface print
# 查看IP地址配置
/ip address print
技术细节解析
IP地址检测机制
脚本支持两种IP获取方式:
- 本地接口检测 (
CFcloud = "false"
)
- 直接读取指定WAN接口的IP地址
- 适用于PPPoE、静态IP等场景
- 性能更好,无需外部依赖
- CloudFlare检测 (
CFcloud = "true"
)
- 使用RouterOS云服务获取公网IP
- 适用于复杂网络环境
- 可能存在延迟或依赖问题
代理状态保持机制
脚本的核心创新在于保持CDN代理设置:
- 状态读取:通过API GET请求获取当前记录的
proxied
状态 - JSON解析:使用RouterOS字符串操作解析JSON响应
- 状态保持:在PUT请求中使用原有的代理设置
- 错误处理:获取失败时默认使用
false
状态
文件系统管理
脚本使用临时文件记录IP变化:
- 初始化检查:首次运行时创建IP记录文件
- 内容解析:读取并清理文件中的IP地址
- 格式处理:去除换行符、回车符、空格等
- 原子更新:删除旧文件后创建新文件
故障排除
常见问题及解决方案
- API认证失败
- 检查Token权限设置
- 验证Zone ID是否正确
- 确认Token未过期
- 记录ID错误
- 重新获取DNS记录列表
- 验证域名拼写
- 检查记录类型是否为A记录
- IP检测异常
- 确认WAN接口名称正确
- 检查接口是否获得公网IP
- 验证RouterOS版本兼容性
- 脚本执行失败
- 开启调试模式查看详细日志
- 检查文件系统权限
- 验证网络连接状态
调试技巧
- 开启调试模式
:local CFDebug "true"
- 手动执行脚本
/system script run batch-ddns
- 查看系统日志
/log print where topics~"script"
性能优化建议
执行频率优化
- 正常场景:5-10分钟间隔检查
- 频繁变化:1-3分钟间隔检查
- 稳定环境:15-30分钟间隔检查
API调用优化
- 在域名较多时增加延迟间隔
- 实施指数退避策略处理API限制
- 考虑使用批量API调用(如果CloudFlare支持)
资源消耗控制
- 定期清理临时文件
- 限制日志文件大小
- 监控脚本执行时间
扩展功能建议
多DNS服务商支持
可以扩展脚本支持其他DNS服务商:
- 阿里云DNS
- 腾讯云DNSPod
- AWS Route 53
- Google Cloud DNS
通知机制
添加IP变化通知功能:
- 邮件通知
- 微信通知
- Telegram通知
- 系统日志集成
高可用性增强
- 多WAN接口支持
- 故障转移机制
- 配置备份与恢复
- 健康检查机制
总结
本文介绍的RouterOS批量DDNS脚本提供了一个完整、可靠的解决方案,特别适合需要管理多个域名解析记录的场景。脚本的核心优势包括:
- 批量处理:单次执行更新多个域名
- 状态保持:保持CloudFlare CDN代理设置
- 自动恢复:具备错误处理和初始化能力
- 调试友好:提供详细的日志和调试信息
通过合理配置和定期维护,该脚本可以显著提高网络管理效率,确保域名解析的及时性和准确性。建议在生产环境使用前进行充分测试,并根据实际需求调整检查频率和超时参数。
重要提示:在使用脚本前,请确保备份现有的DNS配置,并在测试环境中验证脚本功能。API Token应妥善保管,避免泄露给未授权人员。
评论区