0、背景介绍
家里的宽带有动态公网IP,需要DDNS服务实现域名动态解析。
常规方式通过路由器自带的DDNS服务(例如花生壳)就可以了。这种方式要求WAN口必须是公网IP,如果你是用光猫拨号,然后无线路由器连接在光猫上,这种情况你需要在光猫上配置DDNS,而不是你的无线路由器。
如果想要使用自己的域名,可以通过CNAME解析的方式,将自定义域名解析到DDNS提供商提供的域名。
所以此篇文章针对使用RouterOS,但是并不是用RouterOS拨号的情况下DDNS的更新。以我自己为例,家里有多个无线路由器,但是我的RouterOS在我的房间,所以只能使用光猫拨号的方法,所有路由器连接在光猫上。然后将我的RouterOS设置为DMZ主机。这样所有公网的流量都会转发到我的ReouterOS,也就相当于拥有了公网IP。
1、配置思路
最开始,我计划使用在RouterOS较新的版本中,提供的一个Cloud
功能,位于/ip cloud
,但是这个功能似乎需要正版授权版本(我在使用的是试用版),因此不了了之。
而后我又尝试使用/tool dns-update
命令更新解析记录,但是反复实验了很多次,更换了很多参数,都没有成功。最后也放弃了。
最终决定使用这个服务提供商:ChangeIP,它提供了HTTP/HTTPS接口,可以直接通过RouterOS的脚本发送请求。
2、RouterOS脚本
在/system scripts
添加一个脚本,名称我定义为check-and-update-ddns
。
ddnshost对应申请的免费DDNS域名,ddnsuser和ddnspass分别是ChangeIP的用户名和密码。interval表示检测本机公网IP间隔。
这个脚本在运行时,会判断此次检测距离上次检测是否大于3分钟,如果不是就终止此次运行,避免频繁更新。
如果大于3分钟,继续判断此次检测IP和上次检测结果是否一致,一致就无需更新,不一致则将新IP解析记录发送给ChangeIP的接口。
点击运行脚本,测试是否能够完成DNS解析记录的更新。
/systems script add name=au source={脚本内容见下}
脚本内容:
:local ddnshost "example.dynamic-dns.net"
:local ddnsuser "username@mail.com"
:local ddnspass "password"
:local interval [:totime 180s]
:global ddnscheckpoint
:local currenttime [/system clock get time]
:if ([:typeof $ddnscheckpoint] = "time") do={
:log info "DDNS: Last check was $ddnscheckpoint"
} else={
:log info "DDNS: Cannot determine checkpoint, set now."
:global ddnscheckpoint ( $currenttime - 1d )
}
# Get the current IP
:if ($currenttime - $ddnscheckpoint > $interval || $currenttime - $ddnscheckpoint < [:totime 0s]) do={
:log info "DDNS: Performing remote IP detection."
/tool fetch address="ip.changeip.com" host="ip.changeip.com" src-path=("/?" . [/int eth get 0 mac-address ]) dst-path="ip.changeip.com.txt" mode=http port=80
:global ddnscheckpoint $currenttime
} else={
:log info "DDNS: Please be considerate and wait a few seconds longer."
:break
}
# Parse the IP address received from fetch script.
:global ddnslastip
:local html [/file get "ip.changeip.com.txt" contents]
:local ddnsip [:pick $html ([:find $html "<!--IPADDR="] + 11) [:find $html "-->"] ]
# Is it a valid IP and is it different than the last one?
:if ([:typeof [:toip $ddnsip]] = "ip" AND $ddnsip != $ddnslastip ) do={
:log info "DDNS: Sending UPDATE with $ddnsip"
:local ddnsurl ("https://nic.changeip.com/nic/update\3Fip=" . $ddnsip . "&hostname=" . $ddnshost)
/tool fetch url=$ddnsurl user=$ddnsuser password=$ddnspass dst-path=ddns-res.txt
:global ddnslastip $ddnsip
:local resultcode [:pick [/file get ddns-res.txt contents] 0 3]
:if ($resultcode = "200") do={
:log info "DDNS: Updated with $ddnsip"
} else={
:log info "DDNS: Update with $ddnsip failed"
}
} else={
:log info "DDNS: No update required."
}
3、RouterOS定时任务
编写完成上述脚本之后,还需要定期执行这个脚本。添加一个调度任务,每3分钟执行一次脚本。
/system/schedeuler add name=auto-update-ddns \
on-event=check-and-update-ddns \
interval=3m \
comment="check ip and update ddns every 3min"
4、后记
DDNS实现方式太多,使用RouterOS来进行这个操作的一个考虑是作为网关,它很稳定。
另外给大家的建议还有阿里云的CLI,也有封装好的Docker镜像,可以在群晖中使用。
我目前也在这个Docker镜像,但是存在的一个问题是,群晖位于OpenWrt的代理后,有时候获取到的不是我家里的IP,而是代理服务器的IP。现在使用RouterOS脚本就完全没有这种苦恼了。
另外使用计划任务也没有必要检测执行时间。可以精简一下:
:local ddnshost "example.dynamic-dns.net"
:local ddnsuser "username"
:local ddnspass "password"
# Get the current IP
:log info "DDNS: Performing remote IP detection"
/tool fetch address="ip.changeip.com" host="ip.changeip.com" src-path=("/?" . [/int eth get 0 mac-address ]) dst-path="ip.changeip.com.txt" mode=http port=80
# Parse the IP address received from fetch script.
:global ddnslastip
:local html [/file get "ip.changeip.com.txt" contents]
:local ddnsip [:pick $html ([:find $html "<!--IPADDR="] + 11) [:find $html "-->"] ]
# Is it a valid IP and is it different than the last one?
:if ([:typeof [:toip $ddnsip]] = "ip" AND $ddnsip != $ddnslastip ) do={
:log info "DDNS: Sending UPDATE with $ddnsip"
:local ddnsurl ("https://nic.changeip.com/nic/update\3Fip=" . $ddnsip . "&hostname=" . $ddnshost)
/tool fetch url=$ddnsurl user=$ddnsuser password=$ddnspass dst-path=ddns-res.txt
:global ddnslastip $ddnsip
:local resultcode [:pick [/file get ddns-res.txt contents] 0 3]
:if ($resultcode = "200") do={
:log info "DDNS: Updated with $ddnsip"
} else={
:log info "DDNS: Update with $ddnsip failed"
}
} else={
:log info "DDNS: No update required."
}