自己写一个DDNS

前几天借着Github的学生优惠在name.com上嫖了一个域名,这个域名是带SSL的,而恰巧家里的主机因为443端口被封的缘故无法通过Let's Encrypt获得证书,所以把这个域名给家里的主机升级HTTPS再合适不过了。

唯一的问题是name.com只提供静态DNS服务,而服务器放在家里自然是动态IP的。虽然IP不是经常换但是如果换了IP没有及时更新记录就会出问题,何况手动更新记录也有点烦。有两个方案:

  1. 我目前使用的是花生壳的DDNS,这个的记录是动态更新的。我可以在新域名下面新建一条CNAME记录指向动态域名。这样的好处是省事,坏处是可能会增加DNS解析的时间,我目前还不清楚HTTPS要不要求CNAME指向的域名也有证书,如果要求的话这个方法就更不行了。
  2. name.com作为一家比较大的域名商有自己的API以及完备的文档,可以自己写一个定时更新脚本来实现类似DDNS的功能。

经过考虑之后我选择后者。自己写的脚本如下:

#!/bin/sh

domain='<DOMAIN>'
credential='<ACCESS TOKEN>'
ttl=300
interval=10

echo 'Querying type A record ID...'
rec_id=$(curl -su $credential "https://api.name.com/v4/domains/${domain}/records" | jq '.records|map(select(.type=="A"))|.[0].id')
echo 'Found type A record ID:' $rec_id

while true; do
    res_ip=$(host $domain | grep -ohP '\b(?:\d{1,3}\.){3}\d{1,3}\b')
    real_ip=$(curl -s myip.ipip.net | grep -ohP '\b(?:\d{1,3}\.){3}\d{1,3}\b')
    echo 'Resolution IP:' $res_ip 'Real IP:' $real_ip
    if [[ $real_ip != $res_ip ]]; then
        echo "Resolution mismatch! Updating record..."  
        req_data="{\"type\":\"A\",\"fqdn\":\"${domain}.\",\"answer\":\"${real_ip}\",\"ttl\":${ttl}}"
        echo "Request data:" $(echo $req_data | jq '.')
        req_res=$(curl -m 30 -su $credential "https://api.name.com/v4/domains/${domain}/records/${rec_id}" \
            -X PUT -H 'Content-Type: application/json' --data $req_data | jq '.')
        echo "Request result:" $req_res 
        sleep $ttl
    fi
    sleep $interval
done

其实很简单,主要分为一下几个部分:

  1. 记录id的获取。name.com对每一个记录都分配了一个id以便于API操作,这个id在网站管理面板上是不可见的,因此需要在运行时查询,命令为curl -su $credential "https://api.name.com/v4/domains/${domain}/records"。查询之后需要解析JSON,这里我使用的是jq这个第三方JSON parser。
  2. 当前DNS解析的IP。这个使用host结合grep提取IP字符串即可。
  3. 获取本机真实IP。这个接口就多了,我这里用的是myip.ipip.net的接口。据我所知还有接口是直接返回IP字符串的,还可以后处理的功夫。
  4. 如果解析IP和真实IP不符,那就调用API更新记录。注意在更新完之后最好等待TTL的时间以避免更新生效前多次更新。

写完这个脚本再写一个配套的systemd service file,然后systemctl挂在后台运行就好了。运行到现在效果非常好。如果有name.com的域名同时也有类似需求的或许可以参考一下。