Aliyun DNS 定时动态更新脚本

这是一个定时动态的将服务器 IP 更新至 AliDNS,支持 IPv4/IPv6,带 AAAA 记录 / 如果没有 AAAA 记录,就只做 A 或 AAAA 的脚本

文件结构

文件 作用/内容
ali_ddns.sh 主执行脚本
ali_ddns.conf 配置文件
ali_ddns.log 执行日志

配置文件格式

创建 ali_ddns.conf 文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
# Aliyun AccessKey(建议使用子账号最小权限)
ALI_ACCESS_KEY_ID="你的AccessKey ID"
ALI_ACCESS_KEY_SECRET="你的AccessKey Secret"

# 域名 (domain) 和子域 (subdomain) 格式如下
DOMAIN="example.com" # 主域名
SUBDOMAIN="ddns" # 子域名(RR = SUBDOMAIN)
RECORD_TYPE="A" # 记录类型: A 或 AAAA

# 可选项
TTL="600" # TTL,默认为 600
REMARK="XXX 机器的 DDNS" # 记录备注(留空则不设置/不更新备注)
LINE="default" # 解析线路,通常使用 default

脚本配置说明

参数 说明 示例
ALI_ACCESS_KEY_ID 账号AccessKey权限 ID LTAI5tD........peBrF
ALI_ACCESS_KEY_SECRET 账号AccessKey权限SECRET 1SMs1s........ZTAQAI
DOMAIN 主域名 example.com
SUBDOMAIN 子域名 ddns
RECORD_TYPE 记录类型 AAAAA
TTL 生效时间/缓存时长(秒) 600
REMARK DNS 记录备注 HRET DDNS
LINE DNS 解析线路 default

获取 Aliyun AccessKey

  1. 前往 云账号 AccessKey 或者 子账号 AccessKey

  2. 创建子账户(不需要子账户控制权限的话可跳过),可选择使用永久 AccessKey 访问

    子账户选择永久AccessKey后就会分配AccessKey ID和AccessKey Secret注意保存

  3. 配置权限并获取 AccessKey IDAccessKey Secret,权限名称为 AliyunDNSFullAccess

DDNS 脚本

创建 ali_ddns.sh 文件,内容如下:

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
#!/bin/bash

set -euo pipefail

CONFIG_FILE="$1"
[ -z "$CONFIG_FILE" ] && echo "使用方法: $0 <配置文件路径>" && exit 1
[ ! -f "$CONFIG_FILE" ] && echo "配置文件不存在: $CONFIG_FILE" && exit 1
source "$CONFIG_FILE"

need_cmd() {
command -v "$1" >/dev/null 2>&1 || {
echo "缺少命令: $1,尝试安装..."
install_pkg "$1" || { echo "安装 $1 失败"; exit 1; }
}
}

install_pkg() {
if [ -f /etc/debian_version ]; then
apt update -qq && apt install -y -qq "$1"
elif [ -f /etc/redhat-release ]; then
yum install -y -q "$1"
elif command -v opkg >/dev/null 2>&1; then
opkg update >/dev/null 2>&1 || true
opkg install "$1"
else
return 1
fi
}

get_dns_ip() {
local name="$1"
local type="$2"

echo "正在查询DNS记录: $name ($type)" >&2

if command -v dig >/dev/null 2>&1; then
local ip
ip=$(dig +short "$name" "$type" | head -1)
if [[ "$ip" =~ ^[0-9a-fA-F\.:]+$ ]]; then
echo "通过dig获取到DNS IP: $ip" >&2
echo "$ip"
return
fi
fi

if command -v nslookup >/dev/null 2>&1; then
local ip
if [ "$type" = "A" ]; then
ip=$(nslookup "$name" 2>/dev/null | grep "Address:" | tail -1 | awk '{print $2}')
else
ip=$(nslookup -type=AAAA "$name" 2>/dev/null | grep "Address:" | tail -1 | awk '{print $2}')
fi
if [[ "$ip" =~ ^[0-9a-fA-F\.:]+$ ]]; then
echo "通过nslookup获取到DNS IP: $ip" >&2
echo "$ip"
return
fi
fi

echo "DNS查询失败,记录可能不存在" >&2
echo ""
}

# 依赖
need_cmd curl
need_cmd jq
need_cmd openssl

# 必填校验
[ -z "${ALI_ACCESS_KEY_ID:-}" ] && echo "ALI_ACCESS_KEY_ID 未设置" && exit 1
[ -z "${ALI_ACCESS_KEY_SECRET:-}" ] && echo "ALI_ACCESS_KEY_SECRET 未设置" && exit 1
[ -z "${DOMAIN:-}" ] && echo "DOMAIN 未设置" && exit 1
[ -z "${SUBDOMAIN:-}" ] && echo "SUBDOMAIN 未设置" && exit 1
[ -z "${RECORD_TYPE:-}" ] && echo "RECORD_TYPE 未设置" && exit 1

# 默认值
TTL="${TTL:-600}"
LINE="${LINE:-default}"
REMARK="${REMARK:-}"

case "$RECORD_TYPE" in
A) IP_API_URL="https://v4.ipgg.cn/ip"; DNS_TYPE="A" ;;
AAAA) IP_API_URL="https://v6.ipgg.cn/ip"; DNS_TYPE="AAAA" ;;
*) echo "RECORD_TYPE 必须为 A 或 AAAA" && exit 1 ;;
esac

# 定时任务
ensure_cron() {
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SCRIPT_NAME="$(basename "$0")"
CONF_NAME="$(basename "$CONFIG_FILE")"
CRON_EXPR="*/5 * * * * $SCRIPT_DIR/$SCRIPT_NAME $SCRIPT_DIR/$CONF_NAME >> $SCRIPT_DIR/ali_ddns.log 2>&1"

echo "正在检查是否添加定时任务..."
if crontab -l 2>/dev/null | grep -F "$SCRIPT_DIR/$SCRIPT_NAME $SCRIPT_DIR/$CONF_NAME" >/dev/null; then
echo "定时任务已存在,无需添加。"
else
echo "添加定时任务: $CRON_EXPR"
(crontab -l 2>/dev/null; echo "$CRON_EXPR") | crontab -
echo "定时任务添加完成。"
fi
}

# 添加定时任务
ensure_cron

echo "正在获取当前出口IP..."
CURRENT_IP=$(curl -s --max-time 10 "$IP_API_URL" | jq -r '.ip')
[[ ! "$CURRENT_IP" =~ ^[0-9a-fA-F\.:]+$ ]] && echo "获取出口 IP 失败: $CURRENT_IP" && exit 1
echo "当前出口IP: $CURRENT_IP"

record_name="$SUBDOMAIN.$DOMAIN"
DNS_IP=$(get_dns_ip "$record_name" "$DNS_TYPE")

# ===== AliDNS OpenAPI 签名与调用 =====
ALI_ENDPOINT="https://alidns.aliyuncs.com"

urlencode_rfc3986() {
# 按阿里云要求的 RFC3986 百分号编码:空格 -> %20,~ 保持不变
local raw="$1"
python3 - <<'PY' "$raw" 2>/dev/null || \
awk 'BEGIN{
s=ARGV[1];
for(i=1;i<=length(s);i++){
c=substr(s,i,1);
if(c ~ /[A-Za-z0-9\-\_\.\~]/) printf("%s", c);
else printf("%%%02X", ord(c));
}
}
function ord(str, l, r){l=asorti(split(str,a,"")); return and(ascii(str),255)}
function ascii(c){return sprintf("%d", c)}
' "$raw"
PY
}

percent_encode() {
# shell下的实现:使用 Python 优先,缺失时 fallback 到纯bash简易实现
if command -v python3 >/dev/null 2>&1; then
python3 - <<'PY' "$1"
import sys, urllib.parse
s = sys.argv[1]
print(urllib.parse.quote(s, safe='-_.~'))
PY
else
# 简化版(不能覆盖所有字符,但常见参数可用)
local s="$1"
s=${s//'%'/%25}
s=${s//' '/%20}
s=${s//'!'/%21}
s=${s//"\""/%22}
s=${s//'#'/%23}
s=${s//'&'/%26}
s=${s//"'"'/%27}
s=${s//'(''/%28}
s=${s//')'/%29}
s=${s//'*'/%2A}
s=${s//'+'/%2B}
s=${s//','/%2C}
s=${s//'/'/%2F}
s=${s//':'/%3A}
s=${s//';'/%3B}
s=${s//'='/%3D}
s=${s//'?'/%3F}
s=${s//'@'/%40}
s=${s//'['/%5B}
s=${s//']'/%5D}
echo -n "$s"
fi
}

gen_nonce() {
# 尽可能生成随机的 SignatureNonce
if command -v uuidgen >/dev/null 2>&1; then
uuidgen
else
echo "$$-$RANDOM-$(date +%s%N)"
fi
}

aliyun_api_call() {
local action="$1"; shift
# 形如:aliyun_api_call "DescribeSubDomainRecords" "SubDomain=$record_name" "Type=$RECORD_TYPE"
local params=()
params+=("Action=$action")
params+=("Format=json")
params+=("Version=2015-01-09")
params+=("AccessKeyId=$ALI_ACCESS_KEY_ID")
params+=("SignatureMethod=HMAC-SHA1")
params+=("Timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")")
params+=("SignatureVersion=1.0")
params+=("SignatureNonce=$(gen_nonce)")

# 业务参数
for kv in "$@"; do
params+=("$kv")
done

# 参数按键排序
IFS=$'\n' sorted=($(printf '%s\n' "${params[@]}" | LC_ALL=C sort))
unset IFS

# CanonicalizedQueryString
local canonical=""
for kv in "${sorted[@]}"; do
local key="${kv%%=*}"
local val="${kv#*=}"
canonical+="${canonical:+&}$(percent_encode "$key")=$(percent_encode "$val")"
done

# StringToSign
local string_to_sign="GET&%2F&$(percent_encode "$canonical")"

# 计算签名(注意 Key 末尾带 &)
local sign
sign=$(printf '%s' "$string_to_sign" | \
openssl dgst -sha1 -hmac "${ALI_ACCESS_KEY_SECRET}&" -binary | openssl base64)

local url="$ALI_ENDPOINT/?$canonical&Signature=$(percent_encode "$sign")"

curl -s --max-time 15 "$url"
}

# 查询现有记录
echo "正在查询 AliDNS 现有记录..."
resp=$(aliyun_api_call "DescribeSubDomainRecords" "SubDomain=$record_name" "Type=$RECORD_TYPE")

# 记录数组
records_json=$(echo "$resp" | jq -c '.DomainRecords.Record // []')

# 是否已有与当前出口 IP 相同的记录
has_target=$(echo "$records_json" | jq -r --arg v "$CURRENT_IP" '
any(.[]?; .Value == $v)
')

if [ "$has_target" = "true" ]; then
echo "AliDNS 中已存在 $record_name -> $CURRENT_IP,无需更新"
# 仅更新备注
if [ -n "${REMARK}" ]; then
# 找出任一与当前 IP 相同的记录更新备注
target_id=$(echo "$records_json" | jq -r --arg v "$CURRENT_IP" \
'.[] | select(.Value == $v) | .RecordId' | head -n1)
if [ -n "$target_id" ]; then
exist_remark=$(echo "$records_json" | jq -r --arg id "$target_id" \
'map(select(.RecordId == $id)) | .[0].Remark // empty')
if [ "$exist_remark" != "$REMARK" ]; then
echo "备注不一致,更新备注中..."
aliyun_api_call "UpdateDomainRecordRemark" "RecordId=$target_id" "Remark=$REMARK" >/dev/null
echo "备注已更新"
fi
fi
fi
exit 0
fi

record_id=$(echo "$records_json" | jq -r --arg v "$CURRENT_IP" '
.[] | select(.Value != $v) | .RecordId
' | head -n1)

if [ -z "$record_id" ]; then
echo "记录不存在,创建中..."
# RR 即子域名部分(SUBDOMAIN)
add_resp=$(aliyun_api_call "AddDomainRecord" \
"DomainName=$DOMAIN" \
"RR=$SUBDOMAIN" \
"Type=$RECORD_TYPE" \
"Value=$CURRENT_IP" \
"TTL=$TTL" \
"Line=$LINE" \
$( [ -n "$REMARK" ] && echo "Remark=$REMARK" )
)
success_id=$(echo "$add_resp" | jq -r '.RecordId // empty')
if [ -n "$success_id" ]; then
echo "创建成功: $record_name -> $CURRENT_IP (RecordId=$success_id)"
# 双保险:有些环境下 Add 不一定写入备注,追加一次
if [ -n "$REMARK" ]; then
aliyun_api_call "UpdateDomainRecordRemark" "RecordId=$success_id" "Remark=$REMARK" >/dev/null
fi
exit 0
else
echo "创建失败:$add_resp"
exit 1
fi
else
echo "记录存在,RecordId: $record_id,更新中..."
upd_resp=$(aliyun_api_call "UpdateDomainRecord" \
"RecordId=$record_id" \
"RR=$SUBDOMAIN" \
"Type=$RECORD_TYPE" \
"Value=$CURRENT_IP" \
"TTL=$TTL" \
"Line=$LINE"
)
# UpdateDomainRecord 返回 RequestId(无错误即成功)
req_id=$(echo "$upd_resp" | jq -r '.RequestId // empty')
if [ -n "$req_id" ] && ! echo "$upd_resp" | jq -e '.Code' >/dev/null 2>&1; then
echo "更新成功: $record_name -> $CURRENT_IP"
# 备注(若配置了则设置/覆盖一次)
if [ -n "$REMARK" ]; then
aliyun_api_call "UpdateDomainRecordRemark" "RecordId=$record_id" "Remark=$REMARK" >/dev/null
echo "备注已设置"
fi
exit 0
else
echo "更新失败:$upd_resp"
exit 1
fi
fi

使用方法

1
2
chmod +x ali_ddns.sh
./ali_ddns.sh ali_ddns.conf

支持的系统

  • Debian/Ubuntu (使用 apt)
  • CentOS/RHEL (使用 yum)
  • OpenWrt (使用 opkg)

依赖包

脚本会自动安装所需的依赖

  • curl - HTTP请求
  • jq - JSON解析

输出示例

成功创建

1
2
3
4
5
6
7
8
9
10
正在检查是否添加定时任务...
添加定时任务: */5 * * * * /opt/ddns/ali_ddns.sh /opt/ddns/ali_ddns.conf >> /opt/ddns/ali_ddns.log 2>&1
定时任务添加完成。
正在获取当前出口IP...
当前出口IP: 192.168.101.1
正在查询DNS记录: ddns.example.com (A)
通过dig获取到DNS IP: 192.168.101.1
正在查询 AliDNS 现有记录...
记录不存在,创建中...
创建成功: ddns.example.com -> 192.168.101.1 (RecordId=1666.....)

成功更新

1
2
3
4
5
6
7
8
9
10
正在检查是否添加定时任务...
定时任务已存在,无需添加。
正在获取当前出口IP...
当前出口IP: 192.168.101.2
正在查询DNS记录: ddns.example.com (A)
通过nslookup获取到DNS IP: 192.168.101.1
正在查询 AliDNS 现有记录...
记录存在,RecordId: 1666.....,更新中...
更新成功: ddns.example.com -> 192.168.101.2
备注已设置

无需更新

1
2
3
4
5
6
7
8
正在检查是否添加定时任务...
定时任务已存在,无需添加。
正在获取当前出口IP...
当前出口IP: 192.168.101.2
正在查询DNS记录: ddns.example.com (A)
通过dig获取到DNS IP: 192.168.101.2
正在查询 AliDNS 现有记录...
AliDNS 中已存在 ddns.example.com -> 192.168.101.2,无需更新

常见问题

  1. 获取IP失败

    • 检查网络连接
    • 检查是否可以访问 https://v4.ipgg.cn/iphttps://v6.ipgg.cn/ip
  2. API调用失败

    • 检查 AccessKey IDAccessKey Secret 是否正确
    • 检查是否配置 AliyunDNSFullAccess 权限策略
  3. DNS查询失败

    • 检查根域名是否存在
    • 检查 dignslookup 命令是否可用
  4. 权限错误

  • 确保脚本有执行权限:chmod +x ali_ddns.sh
  • 检查执行命令中的配置文件路径是否正确