2

最近入手一台openwrt软路由,开始折腾之旅。
第一件事,先把动态域名映射搞好,好在openwrt集成了阿里DDNS服务,但意想不到的是:这真成了折腾...

折腾的前提:
已申请电信的公网IP
阿里申请的域名,假如为:foo.bar

折腾之旅:
0、我希望阿里DDNS帮我自动更新如下信息:
阿里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的潜规则熟悉了一些(之前使用较少,并且没有系统总结过)。


sswhsz
149 声望4 粉丝