文章

通过OpenWrt热拔插实现内网设备IPv6自动配置DDNS

家中的软路由经常会跑一些自己常用的服务,比如Alist、Syncthing、Jellyfin等,由于不在家中的时候也有访问这些应用的需求,就需要把这些服务对外发布出去,方案首先考虑的就是DDNS了,但现在家庭宽带IPv4已经很难申请到公网IP了,所以IPv4就此作罢。

不过目前运营商已经基本都开了IPv6网络,而且基本都下发了IPv6前缀,所以每一台设备现在都可以配置至少一个IPv6公网地址。那么能搞定IPv6的DDNS也都不错,毕竟现在IPv6已经很普及了,手机4G、5G网络都默认开启了IPv6,只要配置好防火墙策略,把服务端口放出去就行了。

从OpenWrt的仓库里看了一下,有几款DDNS工具,但是都不太符合我的心意,于是自己动手写了一个shell脚本。主要是利用了OpenWrt的热拔插属性,/etc/hotplug.d目录下面有个neigh文件夹,应该是和arp相关的,看了下OpenWrt源码,放置在这个文件夹下的脚本,果然可以在arp添加和老化后执行,而且附带了IP地址和MAC地址的参数,那我就可以把MAC地址和域名关联起来实现DDNS了。

实现的代码如下,把这段代码随意命名,我这里命名为80-arp-ipv6-ddns,然后放置于/etc/hotplug.d/neigh目录下面,然后在OpenWrt管理界面的系统-启动项-本地脚本中重启dnsmasq服务。

下面直接上80-arp-ipv6-ddns脚本的内容,有一些参数需要改成你自己的实际情况,比如CF的相关参数,祝你折腾愉快😄

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#!/bin/sh

CF_TOKEN='xxxxxxxxxxxxxxxxxxxxxxxx' #改成你自己的CF API Token

case $MACADDR in
'52:54:00:d8:04:54') #改成你自己的设备或者虚拟机的MAC地址
    CF_ZONEID='xxxxxxxxxxxxx' #改成你自己的CF区域ID
    NAME='home.taox.in' #你自己的域名
    PROXIED=true
    ;;
'a0:36:9f:50:ae:e9')
    CF_ZONEID='xxxxxxxxxxxxx'
    NAME='speed.taox.in'
    PROXIED=true
    ;;
esac

[ $NAME ] || exit 0

#判断IP地址是否公网IPv6地址
funIsIPV6() {
    PD='0::\/60' #如果56位前缀,改成'00::\/56',一般运营商只下发56或60位前缀。
    IPV6_PD=$(ip -6 route show | grep -E "default from [0-9a-f:]*$PD" | sed -r "s/^default from ([0-9a-f:]*)$PD.*/\1/")

    if [[ $IPADDR =~ ^$IPV6_PD[0-9a-f:]*$ ]]; then
        echo 'YES'
    else
        echo 'NO'
    fi
}

#更新或创建DNS
#参数: $1:域名
funAddDNS() {
    RID=$(echo $records | sed -r "s/.*\"id\":\"([0-9a-f]*)\",\"zone_id\":\"[0-9a-f]*\",\"zone_name\":\"[^\"]*\",\"name\":\"$1\".*/\1/")
    IPV6=$(echo $records | sed -r "s/.*\"name\":\"$1\",\"type\":\"AAAA\",\"content\":\"([0-9a-f:]*)\".*/\1/")
    #如果有匹配的AAAA记录
    if [ ${#IPV6} -lt 40 ]; then
        #如果已经存在的记录和本次添加记录ip地址相同,则跳过。
        [ $IPV6 != $IPADDR ] || return
        #更新DNS记录
        funUpdateDNS $RID $1
    #如果没有匹配的AAAA记录,则创建
    else
        funCreateDNS $1
    fi

}

#更新DNS的http请求
#参数: $1:记录ID $2:域名
funUpdateDNS() {
    curl -s --request PUT \
        --url "https://api.cloudflare.com/client/v4/zones/$CF_ZONEID/dns_records/$1" \
        --header "Content-Type: application/json" \
        --header "Authorization: Bearer $CF_TOKEN" \
        --data "{\"content\":\"$IPADDR\",\"name\":\"$2\",\"type\":\"AAAA\",\"proxied\":$PROXIED}"
}

#创建DNS的http请求
#参数: $1:域名
funCreateDNS() {
    curl -s --request POST \
        --url "https://api.cloudflare.com/client/v4/zones/$CF_ZONEID/dns_records" \
        --header "Content-Type: application/json" \
        --header "Authorization: Bearer $CF_TOKEN" \
        --data "{\"content\":\"$IPADDR\",\"name\":\"$1\",\"type\":\"AAAA\",\"proxied\":$PROXIED}"
}

records=$(curl -s --request GET \
    --url https://api.cloudflare.com/client/v4/zones/$CF_ZONEID/dns_records \
    --header 'Content-Type: application/json' \
    --header "Authorization: Bearer $CF_TOKEN")

case $ACTION in
'add')
    [ $(funIsIPV6) = 'YES' ] || exit 0
    funAddDNS $NAME
    ;;
'remove')
    ;;
esac

本文由作者按照 CC BY 4.0 进行授权