Cloudflare DDNS 更新脚本(脱敏版):多域名 A/AAAA 同步,过滤 ULA 内网 IPv6
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# ========== 配置区 ==========
|
||||
# 认证方式二选一(方式一优先):
|
||||
# 方式一(推荐): My Profile -> API Tokens 创建的受限 Token, 权限 Zone->DNS->Edit
|
||||
API_TOKEN=""
|
||||
# 方式二: Global API Key (My Profile -> API Tokens -> API Keys 页面), 配合账号邮箱
|
||||
API_EMAIL=""
|
||||
API_KEY=""
|
||||
# 每条记录一行, 格式: 域名:类型(:proxied)
|
||||
# 域名 根域 / 子域 / 泛解析均可, 所属 Zone 自动识别(同一账号下一个 Token 即可)
|
||||
# 类型 A=IPv4, AAAA=IPv6
|
||||
# proxied 可选: true=开Cloudflare CDN代理, false=纯解析(默认)
|
||||
# 以后新增域名只需在下面加一行
|
||||
RECORDS=(
|
||||
"lmve.net:A"
|
||||
"lmve.net:AAAA"
|
||||
"*.lmve.net:A"
|
||||
"*.lmve.net:AAAA"
|
||||
"wnfed.com:A"
|
||||
"wnfed.com:AAAA"
|
||||
"*.wnfed.com:A"
|
||||
"*.wnfed.com:AAAA"
|
||||
)
|
||||
# ===========================
|
||||
|
||||
API="https://api.cloudflare.com/client/v4"
|
||||
LOG() { echo "[$(date '+%F %T')] $*"; }
|
||||
|
||||
# 认证头: 优先用受限 Token, 否则用 Global API Key + 邮箱
|
||||
AUTH=()
|
||||
if [ -n "$API_TOKEN" ]; then
|
||||
AUTH=(-H "Authorization: Bearer $API_TOKEN")
|
||||
elif [ -n "$API_EMAIL" ] && [ -n "$API_KEY" ]; then
|
||||
AUTH=(-H "X-Auth-Email: $API_EMAIL" -H "X-Auth-Key: $API_KEY")
|
||||
else
|
||||
LOG "!!! 未配置认证信息 (API_TOKEN 或 API_EMAIL+API_KEY)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# IP 只抓取一次, 多条记录复用
|
||||
CACHE4="" CACHE6=""
|
||||
declare -A ZONE_CACHE
|
||||
|
||||
get_ipv4() {
|
||||
if [ -z "$CACHE4" ]; then
|
||||
CACHE4=$(curl -fsS --max-time 10 -4 https://api.ipify.org 2>/dev/null) || true
|
||||
fi
|
||||
echo "$CACHE4"
|
||||
}
|
||||
|
||||
get_ipv6() {
|
||||
if [ -z "$CACHE6" ]; then
|
||||
# 优先取本机网卡上的公网 IPv6 (2000::/3), 排除 ULA 内网地址 (fc00::/7); 取不到再走外部检测
|
||||
CACHE6=$(ip -6 addr show scope global 2>/dev/null | awk '/inet6/{print $2}' | grep -E '^[23][0-9a-fA-F]{3}:' | head -1 | cut -d/ -f1) || true
|
||||
if [ -z "$CACHE6" ]; then
|
||||
CACHE6=$(curl -fsS --max-time 10 -6 https://api64.ipify.org 2>/dev/null) || true
|
||||
fi
|
||||
fi
|
||||
echo "$CACHE6"
|
||||
}
|
||||
|
||||
# 从记录名推断所属 Zone (逐级剥离左边标签, 匹配最长后缀), 结果缓存
|
||||
get_zone_id() {
|
||||
local rec="$1" cand="$1" resp zid
|
||||
while [ "$(echo "$cand" | tr -cd '.' | wc -c)" -ge 1 ]; do
|
||||
if [ -n "${ZONE_CACHE[$cand]:-}" ]; then
|
||||
echo "${ZONE_CACHE[$cand]}"
|
||||
return 0
|
||||
fi
|
||||
# 含通配符的候选名不可能是 Zone, 直接跳过
|
||||
if [[ "$cand" != *"*"* ]]; then
|
||||
resp=$(curl -fsS -g --max-time 15 "${AUTH[@]}" \
|
||||
"$API/zones?name=$cand&status=active" 2>/dev/null) || true
|
||||
zid=$(echo "$resp" | grep -o '"id":"[0-9a-f]\{32\}"' | head -1 | sed 's/.*:"//;s/"$//')
|
||||
if [ -n "$zid" ]; then
|
||||
ZONE_CACHE[$cand]="$zid"
|
||||
echo "$zid"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
cand="${cand#*.}"
|
||||
done
|
||||
LOG "!!! 找不到 $rec 所属的 Zone"
|
||||
return 1
|
||||
}
|
||||
|
||||
update_record() {
|
||||
local name="$1" type="$2" proxied="${3:-false}" zone_id ip=""
|
||||
type=$(echo "$type" | tr '[:lower:]' '[:upper:]')
|
||||
zone_id=$(get_zone_id "$name") || return 1
|
||||
if [ "$type" = "A" ]; then ip=$(get_ipv4); else ip=$(get_ipv6); fi
|
||||
[ -z "$ip" ] && { LOG "跳过 $name:获取不到 $type 地址"; return 1; }
|
||||
|
||||
local resp rec_id="" cur_ip=""
|
||||
resp=$(curl -fsS -g --max-time 15 "${AUTH[@]}" \
|
||||
"$API/zones/$zone_id/dns_records?type=$type&name=$name" 2>/dev/null) || true
|
||||
rec_id=$(echo "$resp" | grep -o '"id":"[0-9a-f]\{32\}"' | head -1 | sed 's/.*:"//;s/"$//')
|
||||
cur_ip=$(echo "$resp" | grep -o '"content":"[^"]*"' | head -1 | sed 's/.*:"//;s/"$//')
|
||||
|
||||
if [ -n "$rec_id" ] && [ "$cur_ip" = "$ip" ]; then
|
||||
LOG "$type $name 未变化 ($ip),跳过"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local method="POST" url="$API/zones/$zone_id/dns_records"
|
||||
[ -n "$rec_id" ] && { method="PUT"; url="$url/$rec_id"; }
|
||||
|
||||
local body="{\"type\":\"$type\",\"name\":\"$name\",\"content\":\"$ip\",\"ttl\":120,\"proxied\":$proxied}"
|
||||
if curl -fsS -g --max-time 15 -X "$method" "${AUTH[@]}" \
|
||||
-H "Content-Type: application/json" -d "$body" "$url" 2>/dev/null \
|
||||
| grep -q '"success":true'; then
|
||||
LOG "$type $name -> $ip 更新成功"
|
||||
else
|
||||
LOG "!!! $type $name 更新失败"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
for entry in "${RECORDS[@]}"; do
|
||||
IFS=':' read -r name type proxied <<< "$entry"
|
||||
update_record "$name" "${type:-A}" "${proxied:-false}" || true
|
||||
done
|
||||
}
|
||||
main
|
||||
Reference in New Issue
Block a user