RouterOS v7通过MAC地址获取IPv6并上报至DNSPod

2,586 阅读3分钟

在我将家里的主路由改为RouterOS后,始终找不到一个能完美更新内网设备IPv6的脚本,要么是通过EUI64转换的,要么就是端口转发,还有引入第三个设备,用别的语言去做DDNS的更新,这些方案虽然都能实现最终的效果,但无疑是增加了别的成本,而且不够优雅。RouterOS本身就支持的东西,为什么要舍近求远并?这并不是我的风格。

在网络上搜索了一周后,还真发现了有一个相关的脚本,但是整体看着较乱,作者也是在RouterOS v6上写的,所以也不敢运行。最终各种查阅无果后,只有自己动手写这个脚本了。

那先说这个脚本的需求: 运行在RouterOS上,自动获取指定内网设备的IPv6地址,具有自动更新、定时检查等功能;

同时,这篇文章也发布在我自己的博客:RouterOS v7通过MAC地址获取IPv6并上报至DNSPod

1. 开始之前

在开始写之前,我们要知道 RouterOS 脚本使用的是 MikroTik RouterOS 特定的脚本语言,它是一种专为 MikroTik 路由器设备设计的脚本语言。这个脚本语言被用于配置和自动化 MikroTik 路由器上的各种任务和功能。这种脚本语言与一般的编程语言略有不同,具体的语法和功能集是为了满足 MikroTik RouterOS 的特定需求而设计的。所以,在开始之前我们需要了解基本的语法:Scripting - RouterOS ,不过幸运的是,这个语法并不难。

2. 脚本编写

通过上面的需求,我们首先要获取到内网设备的IPv6地址,才有后续的更新操作

2.1 获取Neighbor id

通过查找文档找到了获取所有内网设备的IPv6地址的位置是:IPv6 -> Neighbors目录,那么映射在命令上就是:

:local ipAddressList [/ipv6 neighbor find]

但这段命令获取的是所有设备的IPv6地址,很明显不符合我们的要求,因为我们是指定内网设备,那指定这个内网设备用什么指定?有网络别名、IPv4地址,但以上两个有可能冲突,也有可能变化,唯一不变的就是MAC地址了,那我们指定MAC地址:

# 指定MAC地址
:local targetMac "12:34:56:69:B1:C3"

# 通过MAC地址筛选内网IPv6地址
:local ipAddressList [/ipv6 neighbor find mac-address=$targetMac]

以上,我们就可以获取到指定设备的Neighbor Id了,这是个数组,为什么是一个数组?因为设备的IPv6可能是多个。

2.2 获取设备IPv6地址

我们已经知道获取Neighbor Id,返回给我们的是一个数组,那么我们具体该上报哪一个IPv6地址,就有些说法了,因为IPv6地址的下发,是根据运营商提供的IPv6前缀,有的时候我们重启路由器,通过老前缀下发的IPv6地址是不会立马消失不见的,这个地址还会存活一段时间,但我们肯定不是上报已经不用的IPv6地址,而是新前缀下发的地址。

同时,路由器一般还会下发一个fe80的内网地址(也可能是别的开头),那这就需要我们甄别哪一个地址才是我们需要上报的DNS的。

# 获取下发的IPv6地址前缀
:local ipv6Prefix [/ipv6 dhcp-client get [find interface=$wanInterface status=bound] prefix]

# 获取IPv6 并截取前缀
:local cidrNetwork [:pick $ipv6Prefix 0 ([:find $ipv6Prefix "::" -1] + 1)]

# 循环遍历查找对应的ipv6地址
:foreach neighborId in=$ipAddressList do={
    # 获取邻居的IPv6地址和MAC地址
    :local ipv6Address [/ipv6 neighbor get $neighborId address]
    :local ipv6Status [/ipv6 neighbor get $neighborId status]
    :local foundPosition [:find $ipv6Address $cidrNetwork]

    :if ($foundPosition >= 0 && $ipv6Status = "reachable") do={
        :set ipv6Report $ipv6Address;
        :log info ("暂存:" . $ipv6Address)
    } else={
        :log info ("丢弃:" . $ipv6Address)
    }
}

在上面代码中就是解决这个问题,首先去拿拨号接口的地址前缀,在循环设备IPv6地址时进行对比,只有对比成功的地址才会被暂存起来,用于后面的上报。

那么到这里,我们就拿到了后续要上报的设备IPv6地址$ipv6Address

2.3 将IPv6上报到DNS服务商

以下是完整脚本代码,这里我用的DNS服务商是DNSPOD,上报的代码网上有很多,我直接复制就不单独实现了(阿里云可以自己去找),至于如何配置脚本及定时任务,网上教程很多,在这里我就不展开细讲了。

# 在这里替换成你需要的内网设备的MAC地址
:local targetMac "12:34:56:69:B1:C3"

# 定义自己的需更新的拨号接口
:local wanInterface "pppoe-out1"

# DNSPOD token
:local token "id,token"

# 域名
:local domain "domian.com"

# 二级域名
:local subdomain "sub"


###### 分隔符,以下不需要做任何操作 ######

# 获取下发的IPv6地址前缀
:local ipv6Prefix [/ipv6 dhcp-client get [find interface=$wanInterface status=bound] prefix]

# 获取IPv6 并截取前缀
:local cidrNetwork [:pick $ipv6Prefix 0 ([:find $ipv6Prefix "::" -1] + 1)]

# 存储最终上报的IPv6地址
:local ipv6Report ""

# 查找指定MAC地址的设备
:local ipAddressList [/ipv6 neighbor find mac-address=$targetMac]

# 循环遍历查找对应的ipv6地址
:foreach neighborId in=$ipAddressList do={
    # 获取邻居的IPv6地址和MAC地址
    :local ipv6Address [/ipv6 neighbor get $neighborId address]
    :local ipv6Status [/ipv6 neighbor get $neighborId status]
    

    :local foundPosition [:find $ipv6Address $cidrNetwork]

    :if ($foundPosition >= 0 && $ipv6Status = "reachable") do={
        :set ipv6Report $ipv6Address;
        :log info ("暂存:" . $ipv6Address)
    } else={
        :log info ("丢弃:" . $ipv6Address)
    }
}

# 域名拼接
:local dname ($subdomain.".".$domain)

# 获取域名列表
:local record [/tool fetch url="https://dnsapi.cn/Record.List" http-data="login_token=$token&format=json&domain=$domain&sub_domain=$subdomain&record_type=AAAA" as-value output=user]

# 获取域名 id 和 ip
:set record ($record->"data")
:set record [:pick $record [:find $record "\"records\":"] [:len $record]]
:local recordid [:pick $record ([:find $record "\"id\":\""]+6) [:find $record "\",\"ttl"]]
:local recordip [:pick $record ([:find $record "\"value\":\""]+9) [:find $record "\",\"en"]]

# 更新 ip 地址
:if ($recordip!=$ipv6Report && [:len $ipv6Report] > 0) do={
    /tool fetch url="https://dnsapi.cn/Record.Ddns" http-data="login_token=$token&format=json&domain=$domain&sub_domain=$subdomain&record_id=$recordid&record_line_id=0&value=$ipv6Report"
    :log info ("[".$dname. " -> " . $ipv6Report ."] 已更新")
} else={
    :log info ("[".$dname."] 无需更新")
}

屏幕截图 2023-09-29 161227.png

3. 参考资料

ROS实现DNSPod的DDNS服务脚本