家里有一台使用矿机星际蜗牛搭建的NAS,主路由使用的是Mikrotik RB750GR3,NAS是直接连在主路由LAN口上,现在各大运营商已经不给IPV4,好在电信还能给公网的IPV6,虽然是动态,但好歹也提供了一个公网访问的途径。

前提

  • 运营商给了IPV6或IPV4,本文以IPV6为版本

  • 购买了域名

  • 使用RouterOS路由器

路由器配置

防火墙配置

RouterOS的IPV6防火墙默认情况下是会拦截公网进来的流量的。因此需要先建个入站规则。在ipv6->Firewall菜单下,新建一条Rule:

dst-address填你NAS获取到的公网ipv6地址,一般是240开头。

image-20220504200704942

注意,这条Rule一定要放在最后一条drop上面,建议comment配置为一个固定值,后续脚本会用。

这条配完后,使用手机4/5G网络应该就可以ping通NAS了。

但是NAS的ipv6是会变的,所以要写脚本动态更新这条Rule

当然也可以把防火墙直接开了,但是这样相当于子网下的所有ipv6设备都暴露在外网了,可能会有安全隐患,不建议ipv6防火墙全开。

脚本配置

因为Router自带的fetch工具实在是太弱了,完全满足不了各大域名厂商复杂的API接口,因为需要在NAS上写一个HTTP服务来完成DDNS,下面是SpringBoot Controller代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@RequestMapping("/ddns")
fun ddns(@RequestParam(name = "ipv6") ipv6: String, @RequestParam(name = "rr") rr: String?): String {
//路由器会传ipv6和要更新的rr记录过来,下面执行更新操作就行了
log.info("update ddns:ipv6=$ipv6,rr=$rr")
val noSuffixIpv6 = ipv6.split("/")[0]
runCatching {
val config = Config() // 您的AccessKey ID
.setAccessKeyId("***") // 您的AccessKey Secret
.setAccessKeySecret("***")
// 访问的域名
// 访问的域名
config.endpoint = "alidns.cn-hangzhou.aliyuncs.com"
val client = Client(config)
val response =
client.describeDomainRecords(DescribeDomainRecordsRequest().apply { domainName = "yourdomain.top" })
for (domain in response.getBody().domainRecords.record) {
val domainRR = domain.rr
if (rr?.contains(domainRR) == true)
if (noSuffixIpv6 != domain.value) {
val request = UpdateDomainRecordRequest()
request.recordId = domain.recordId
request.value = noSuffixIpv6
request.type = domain.type
request.rr = domain.rr
request.priority = domain.priority
request.ttl = domain.ttl
request.line = domain.line
client.updateDomainRecord(request)
log.info("update ddns:${rr}.yourdomain.top ip ${domain.value} ->$noSuffixIpv6")
} else {
log.info("本次监听外网IP没有变更!");
}
}
return "ok"
}.onFailure {
log.error("update ddns failure", it)
var msg = "获取到外网IP:$ipv6\n更新过程中出现异常!\n"
val w = StringWriter()
it.printStackTrace(PrintWriter(w))
msg += w.toString()
msg = msg.replace("\n", "<br/>")
//邮件通知
MailHelper.sendTextMail("DDNS更新失败!", content = msg)
}
return "failure"
}

其实只需要实现一个能动态更新域名厂商的http服务就行,不管是什么语言写的都可以。

下面是RouterOS脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
:global ddnsflag;
:local a [/ipv6 pool get [find name=ipv6] prefix];
:local suffix [:pick $a 0 ([:len $a]-4)];
:local rs "$(suffix)211:32ff:fe11:2233/128";
/ipv6 firewall filter;
:local n [find comment ~ "nas"];
:local b [get number=$n dst-address];
:if (rs!=b) do={
:global ddnsflag true;
}
:if (ddnsflag!=false) do={
/ipv6 firewall filter set numbers=([find comment ~ "nas"]) dst-address=$rs;
:log warning message="new nas ip $(rs)";
:local result [/tool fetch url="http://192.168.1.22:8080/cloud/debug/ddns?ipv6=$(rs)&rr=xxxx" as-value output=user];
:if ($result->"status" = "finished") do={
:if ($result->"data" = "\"ok\"") do={
:log warning message="update ddns success";
:global ddnsflag false;
}
}
}
}

其中第四行后面的4位需要根据NAS上已经获取到的地址同步更改,原理是ipv6有个EUI-64规定,可以根据mac来生成后面,因此动态ipv6变化的只是前64位,后64位是固定的,因此这里直接截取前面的,然后拼起来就得到了NAS的地址。我们可以在IPv6 Address选项下看到该配置。

image-20220504204212671

脚本会同步修改Firewall,实现方案是先用comment找到那条Rule,然后再重写设置Dst Address。可能有其他简单办法,但是这个方法工作也挺OK的。

其中fetch的地址修改成你自己写的服务地址,把获取到的ipv6和想更新的rr传过去即可,然后具体更新逻辑都是在http服务里实现的。更新成功返回ok,脚本会更新标志位,下次地址变化之前不会重写调用服务。

设置脚本定期运行

很简单,在System->Scheduler建个周期任务就行了,每分钟运行一次。

最后

这个方案还有一个优点,IPv6一定是正确的,如果NAS通过自己获取自己的IPv6的话,有可能会获取到很多个IPv6地址,而不知道哪个是正确的。