DDNS 自动更新脚本

发布于 2023-10-29  11 次阅读


需求

  • 尽量不影响主路由(折腾全在旁路由)
  • 从主路由中获取外网 IP 地址(不使用网站检测,因为我知道 IPV4/IPV6 是公网,而且也不想频繁访问网络)
  • 更新成功或脚本运行错误时,能收到提示

脚本

基于上面的说明,直接贴出脚本代码(使用 CloudFlare API),通过 SSH 授权密钥访问主路由获取 IPV4/IPV6 后与本地文件对比 IP 变化判断是否有更新,自用,有需要大佬可以参考修改(此脚本在 OpenWrt 中验证通过):

#!/bin/sh

set -e

IP_FILE="/root/auto_ddns/ip.txt"
ID_FILE="/root/auto_ddns/cloudflare_id.txt"
LOG_FILE="/root/auto_ddns/ddns.log"

check_command() {
    if ! command -v "$1" &> /dev/null; then
        echo "Error: \"$1\" command not found"
        exit 1
    fi
}

check_command "curl"
CURL_BIN="/usr/bin/curl"

handle_error() {
	exec >/dev/tty 2>&1

    # echo "$(<&2)" >> "$LOG_FILE"
	$CURL_BIN \
		-T "$LOG_FILE" \
		-H "Authorization: Bearer tk_zGKbk7wDA************" \ # 认证
		-H "Title: NAS DDNS 更新脚本执行错误" \
		-H "X-Priority: 4" \
		-H "Tags: globe_with_meridians" \
		-H "Filename: ddns.log" \
		https://ntfy.example.com/Task # ntfy 消息服务地址
}

trap 'handle_error' ERR

echo "" > $LOG_FILE

exec > >(tee -a "$LOG_FILE") 2>&1

log() {
	if [ "$1" ]; then
		echo -e "[$(date +'%Y-%m-%d %H:%M:%S')] - $1" >> $LOG_FILE
	fi
}

# cloudflare api
AUTH_EMAIL="your@email.com" # cloudflare 注册邮箱地址
AUTH_KEY="fi8uiKCB*************************" # found in cloudflare account settings | Zone API TOKEN
ZONE_NAME="example.com" # 域名
RECORD_NAME="ddns.example.com" # 需要更新的 DDNS 记录名
TTL=120 # 设 1 为自动

ROUTER_IP="192.168.12.1" # 主路由地址
ROUTER_PORT="22" # 主路由 SSH 端口号

log "开始执行!"

# 获取路由器 ip 地址
IPV4=$(ssh -i '/root/auto_ddns/id_ed25519' admin@${ROUTER_IP} -p ${ROUTER_PORT} ifconfig ppp0 | grep 'inet addr'  | awk '{print $2}' | awk -F ':' '{print $2}')
# IPV6 地址由于都是公网的,获取 OpenWrt 自身即可
IPV6=$(ifconfig br-lan | grep inet6 | grep -v fe80:: | awk '{print $3}' | awk -F '/' '{print $1}')
# 如果有多组 IPV6 则取最后一组
IPV6=$(echo $IPV6 | awk '{print $NF}')

if [ -f $ID_FILE ] && [ $(wc -l $ID_FILE | cut -d " " -f 1) == 3 ]; then
	log "发现 ID 文件,将读取内容"
	zone_id=$(sed -n '1p' $ID_FILE)
	v4_record_id=$(sed -n '2p' $ID_FILE)
	v6_record_id=$(sed -n '3p' $ID_FILE)
else
	log "未存在 ID 文件,将创建并写入内容"
	# get zone id
	zone_id=$($CURL_BIN -s -X GET "https://api.cloudflare.com/client/v4/zones?name=$ZONE_NAME" -H "Authorization: Bearer $AUTH_KEY" -H "Content-Type: application/json" | sed -E "s/.+\"result\":\[\{\"id\":\"([a-f0-9]+)\".+/\1/g" )

	# get record response
	record_response_json=$($CURL_BIN -s -X GET "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records?name=$RECORD_NAME" -H "Authorization: Bearer $AUTH_KEY" -H "Content-Type: application/json")

	# get both v4 and v6 record ip
	v4_record_ip=$(echo $record_response_json | sed -E "s/.+\"content\":\"([a-f0-9.]+)\".+\"proxiable\".+/\1/g")
	v6_record_ip=$(echo $record_response_json | sed -E "s/.+\"content\":\"([a-f0-9:]+)\".+\"proxiable\".+/\1/g")

	# get both v4 and v6 record id
	v4_record_id=$(echo $record_response_json | sed -E "s/.+\{\"id\":\"([a-f0-9]+)\".+\"type\":\"A\".+/\1/g")
	v6_record_id=$(echo $record_response_json | sed -E "s/.+\{\"id\":\"([a-f0-9]+)\".+\"type\":\"AAAA\".+/\1/g")

	# write to file
	echo "$zone_id" > $ID_FILE
	echo "$v4_record_id" >> $ID_FILE
	echo "$v6_record_id" >> $ID_FILE
fi

if [ ! -f $IP_FILE ] || [ $(wc -l $IP_FILE | cut -d " " -f 1) != 2 ]; then
	log "未存在 IP 文件,将创建并写入值"
	if [ -z "$v4_record_ip" ] || [ -z "$v6_record_ip" ]; then
		log "ip 记录值为空(将从 CloudFlare 中获取)"

		# get record response
		record_response_json=$($CURL_BIN -s -X GET "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records?name=$RECORD_NAME" -H "Authorization: Bearer $AUTH_KEY" -H "Content-Type: application/json")
		v4_record_ip=$(echo $record_response_json | sed -E "s/.+\"content\":\"([a-f0-9.]+)\".+\"proxiable\".+/\1/g")
		v6_record_ip=$(echo $record_response_json | sed -E "s/.+\"content\":\"([a-f0-9:]+)\".+\"proxiable\".+/\1/g")
	fi
	echo "ip 记录值将写入 IP 文件中"
	echo "$v4_record_ip" > $IP_FILE
	echo "$v6_record_ip" >> $IP_FILE
fi

OLD_IPV4=$(sed -n '1p' $IP_FILE)
OLD_IPV6=$(sed -n '2p' $IP_FILE)

if [ $OLD_IPV4 == $IPV4 ]; then
	log "IPV4 未发生变化"
else
	update_v4=$($CURL_BIN -s -X PUT "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records/$v4_record_id" -H "Authorization: Bearer $AUTH_KEY" -H "Content-Type: application/json" --data "{\"id\":\"$zone_id\",\"type\":\"A\",\"name\":\"$RECORD_NAME\",\"content\":\"$IPV4\",\"ttl\":$TTL}")
	success=$( echo $update_v4 | sed -E "s/.+\"success\":[ ]*([a-z]+).+/\1/g")
	if [ -n $update ] && [ $success == "true" ]; then
		echo "$IPV4" > $IP_FILE
		log "IPV4 从 ${OLD_IPV4} -> ${IPV4} 更新成功"
		message="IPV4 更新成功:\n旧:${OLD_IPV4}\n新:${IPV4}\n"
	else
		log "IPV4 API 更新失败,详细信息:\n$update_v4"
		message="IPV4 API 更新失败,详细信息:\n$update_v4\n"
	fi
fi

if [ $OLD_IPV6 == $IPV6 ]; then
	log "IPV6 未发生变化"
else
	update_v6=$($CURL_BIN -s -X PUT "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records/$v6_record_id" -H "Authorization: Bearer $AUTH_KEY" -H "Content-Type: application/json" --data "{\"id\":\"$zone_id\",\"type\":\"AAAA\",\"name\":\"$RECORD_NAME\",\"content\":\"$IPV6\",\"ttl\":$TTL}")
	success=$( echo $update_v6 | sed -E "s/.+\"success\":[ ]*([a-z]+).+/\1/g")
	if [ -n $update ] && [ $success == "true" ]; then
		echo "$IPV6" >> $IP_FILE
		log "IPV6 从 ${OLD_IPV6} -> ${IPV6} 更新成功"
		message="${message}IPV6 更新成功:\n旧:${OLD_IPV6}\n新:${IPV6}\n"
	else
		log "IPV6 API 更新失败,详细信息:\n$update_v6"
		message="${message}IPV6 API 更新失败,详细信息:\n$update_v6\n"
	fi
fi

if [ ! -z "$message" ]; then
	$CURL_BIN \
		-H "Authorization: Bearer tk_zGKbk7wDA************" \ # 认证
		-H "Title: NAS DDNS 更新" \
		-H "Tags: globe_with_meridians" \
		-d "$(echo -e $message)" \
		https://ntfy.example.com/Task # ntfy 消息服务地址
fi

log "执行完毕!"

保存后,赋予该脚本可执行权限:

chmod +x /root/auto_ddns/ddns.sh

然后 crontab -e 打开定时任务,每隔 5 分钟运行一次:

*/5 * * * * /root/auto_ddns/ddns.sh

感谢

参考了下面的脚本,感谢!

https://gist.github.com/0neday/04141ba4d3ac3ccf77a5b5837b104762