最近入手一台openwrt软路由,开始折腾之旅。
第一件事,先把动态域名映射搞好,好在openwrt集成了阿里DDNS服务,但意想不到的是:这真成了折腾...
折腾的前提:
已申请电信的公网IP
阿里申请的域名,假如为:foo.bar
折腾之旅:
0、我希望阿里DDNS帮我自动更新如下信息:
1、在定时任务中,查询到 阿里DDNS的执行命令# crontab -l
,结果如下:*/10 * * * * /usr/sbin/aliddns >> /var/log/aliddns.log 2>&1
找到执行脚本:/usr/sbin/aliddns
2、启用脚本调试信息输出:
在脚本第一行命令前加上:
# 输出调试信息(执行的命令)
set -x
# 调试信息格式(打印行号)
PS4='+${LINENO}: '
后来反复执行调试脚本中,urlencode部分输出过于冗长,影响阅读,可以针对urlencode函数执行前后加上:set +x
关闭调试、set -x
打开调试,这样整个命令执行过程就清晰多了。
3、定位到第一个问题:
<h1>Not Found</h1>The requested URL /dyndns/getip was not found on this server
原来是 获取公网ip地址时候,使用了下面的代码:
intelnetip() {
tmp_ip=`curl -sL --connect-timeout 3 members.3322.org/dyndns/getip`
if [ "Z$tmp_ip" == "Z" ]; then
tmp_ip=`curl -sL --connect-timeout 3 api-ipv4.ip.sb/ip`
fi
echo -n $tmp_ip
}
把命令拿出来单独执行:curl members.3322.org/dyndns/getip
原来members.3322.org网站已经停止了公网IP服务,返回了 404 header。但在脚本中根据返回值是否为空来判断执行成功还是失败,显然发生了误判。
这样,只要把这一行代码注释掉即可。
4、继续执行,又碰到了错误:
curl: (3) Error
2022-08-28 23:10:00 ADD record
2022-08-28 23:10:00 # ERROR, Please Check Config/Time
难道是系统时间有问题吗?核对了下系统时间,似乎没看到什么问题。无奈之下,需要仔细查看阿里云解析API了。同时,为了方便修改bash脚本,将 aliddns拷贝到一个临时目录方便修改执行。
5、阿里云解析API文档:https://help.aliyun.com/docum...
仔细查看了文档,大概有如下规则:
以RPC调用规则为例:
- 请求参数包括公共请求参数、业务请求参数。
- 将参数按字母顺序排序、将参数名/参数值分别按 UTF-8编码后进行urlencode,得到“规范化的查询字符串”
- “规范化的查询字符串”添加上:请求Method(比如GET)、秘钥信息(AccessSecret),使用 HMAC-SHA1 计算消息摘要,并作为最后一个参数补充到URL请求上。
6、用Java实现需要用到的3个API:
- DescribeSubDomainRecords 获取解析记录列表
- AddDomainRecord 添加解析记录
- UpdateDomainRecord 修改解析记录
这之间继续碰到如下几个问题:
- 冷门点之一:HMAC-SHA1消息摘要算法:和MD5消息摘要使用JDK自带的通用的 java.security.MessageDigest不同,HMAC-SHA1没有包含在MessageDigest中,而是通过jce扩展包:javax.crypto.Mac提供。(Mac具有和MessageDigest几乎完全相同的API接口)
- 冷门点之二:时间戳需要指定UTC时间
- 弯路之一:JDK自带的URLEncoder没有对'*'字符进行编码。而按照阿里文档要求(按RFC3986规则编码),其中对'*'字符也要编码为:'%2A',这一点经过反复调用、比对请求返回结果才发现。
- 弯路之二:UpdateDomainRecord用到两个参数 RR 和 RecordId,一开始以为 RecordId 参数在前,RR在后面;经过调用对比,才发现 RR参数排序是在前面的。原来忽略了字母大小写问题。所有大写字母,其ASCii码都小于小写字母。
按以下方法放开java URLEncoder对"*"字符的编码:
// java URLEncoder并没有提供用户指定、修改“免编码字符”的机会。下面方法的修改,影响是全局的,仅仅中测试环境下使用。
Field field = URLEncoder.class.getDeclaredField("dontNeedEncoding");
field.setAccessible(true);
BitSet dontNeedEncoding = (BitSet) field.get(null);
dontNeedEncoding.clear('*');
几个Java API的编写终于调通了,通过Java api的实现,也理清了调用阿里dns云解析api所需要的基本操作。继续回到aliddns脚本排查中。
7、aliddns脚本的主要逻辑:
openwrt提供了称为 uci的统一配置接口,openwrt的阿里ddns配置信息,即通过 uci存储在 /etc/config/aliddns文件中。脚本的主要逻辑如下:
- 通过uci读取阿里ddns配置
- 通过第三方api获取本机WAN口公网IP
- 通过nslookup命令从域名服务中查询域名映射的ip
- WAN口ip和路由器查询ip,如果不同,则调用DescribeSubDomainRecords获取解析记录、并删除之;再添加新的解析记录
8、查询解析记录失败问题:
-
通过uci读取阿里ddns配置中,子域名不正确(这里子域名的配置为“*”)。发现问题出在下面代码:
uci_get_by_name() { local ret=$(uci get $NAME.$1.$2 2>/dev/null) echo ${ret:=$3} }
当读取子域名时,实际执行
echo *
,在bash中执行后,返回结果类似于ls
命令一样,列出了当前目录下所有文件。当然拿到了错误的结果。只需要改为:echo "${ret:=$3}"
即可。 -
发送DescribeSubDomainRecords查询请求时,子域名的“*”未编码,不符合阿里api要求。
query_recordid() { send_request "DescribeSubDomainRecords" "SignatureMethod=HMAC-SHA1&SignatureNonce=$timestamp&SignatureVersion=1.0&SubDomain=$sub_dm.$main_dm&Timestamp=$timestamp&Type=A" }
核心的是
&SubDomain=$sub_dm.$main_dm
,这时候需要对参数值$sub_dm.$main_dm
进行urlencode,改为:&SubDomain=$(urlencode $sub_dm.$main_dm)
但!修改后,居然*.foo.bar
被编码成了%61%74.foo.bar
,decode结果是at.foo.bar
??? 谁清楚这是怎么回事?
9、至此,我被折磨的有点崩溃了。几乎想放弃在阿里ddns中使用 '*'泛域名解析了。
好在!既然'*'字符读取和encode存在问题,那么直接将'*'字符配置为'%2A'怎么样呢?感觉应该是可以的。试一下!真的解析成功了!
好吧!总结一下:只需要注意两点:
1. 将/usr/sbin/aliddns脚本中members.3322.org这一行代码注释
2. 阿里ddns服务中,子域名如果是"*",可以写作"%2A" (其他一般化二级域名实测可以正常更新)
记录一下这一段从折腾到被折磨的历程吧。不过从中倒是也有一些收获。比如:之前从未调用过阿里云api、某些api的比较冷门的使用、bash的潜规则熟悉了一些(之前使用较少,并且没有系统总结过)。