This commit is contained in:
2025-11-10 10:27:07 +08:00
commit be9295310c
2 changed files with 610 additions and 0 deletions

514
ddns.sh Normal file
View File

@@ -0,0 +1,514 @@
#!/bin/bash
# --- 脚本元信息与颜色定义 ---
GREEN="\033[32m"
RED="\033[31m"
YELLOW="\033[0;33m"
NC="\033[0m"
GREEN_ground="\033[42;37m" # 全局绿色
RED_ground="\033[41;37m" # 全局红色
Info="${GREEN}[信息]${NC}"
Error="${RED}[错误]${NC}"
Tip="${YELLOW}[提示]${NC}"
# --- 脚本欢迎界面 ---
cop_info(){
clear
echo -e "${GREEN}#######################################################
# ${RED}Debian DDNS 一键脚本 (最终修正版)${GREEN} #
# 作者: ${YELLOW}LaoWang & AI ${GREEN}#
# https://blog.wlens.top ${GREEN}#
# ${YELLOW}优化: 缓存ZoneID, 重构更新逻辑, 加固文件权限${GREEN} #
#######################################################${NC}"
echo -e "${Info}此版本已修复所有已知BUG感谢您的耐心。 "
echo
}
# --- 环境检查 ---
# 检查系统
if ! grep -qiE "debian|ubuntu" /etc/os-release; then
echo -e "${Error}本脚本仅支持 Debian 或 Ubuntu 系统。"
exit 1
fi
# 检查root权限
check_root(){
if [[ $(whoami) != "root" ]]; then
echo -e "${Error}请以root身份执行该脚本"
exit 1
fi
}
# 检查并安装curl和jq
check_curl() {
if ! command -v curl &>/dev/null || ! command -v jq &>/dev/null; then
echo -e "${YELLOW}未检测到 curl 或 jq正在安装...${NC}"
apt-get update && apt-get install -y curl jq
if [ $? -ne 0 ]; then
echo -e "${RED}安装 curl/jq 失败,请手动安装后重试。${NC}"
exit 1
fi
fi
}
# --- 核心安装与文件生成 ---
# 安装DDNS相关文件
install_ddns(){
# 备份旧版本,避免覆盖
if [ -d "/etc/DDNS" ]; then
echo -e "${Tip}检测到已存在的DDNS目录将备份为 /etc/DDNS.bak_$(date +%s)"
mv /etc/DDNS "/etc/DDNS.bak_$(date +%s)" 2>/dev/null
fi
# 创建工作目录
mkdir -p /etc/DDNS
# 将此脚本自身复制为系统命令
cp "$0" /usr/bin/ddns && chmod +x /usr/bin/ddns
# 创建纯净的配置文件 (仅包含用户可配置项)
cat <<'EOF' > /etc/DDNS/.config
Domain="your_domain.com"
Domainv6="your_domainv6.com"
Email="your_email@gmail.com"
Api_key="your_api_key"
Telegram_Bot_Token=""
Telegram_Chat_ID=""
EOF
chmod 600 /etc/DDNS/.config # 安全加固:设置配置文件权限
# 创建独立的IP状态文件
touch /etc/DDNS/.old_ipv4 && chmod 600 /etc/DDNS/.old_ipv4
touch /etc/DDNS/.old_ipv6 && chmod 600 /etc/DDNS/.old_ipv6
# ================================================================= #
# 创建全新的、语法完全正确的DDNS执行脚本
# ================================================================= #
cat <<'EOF' > /etc/DDNS/DDNS
#!/bin/bash
# DDNS工作目录
WORK_DIR="/etc/DDNS"
# 日志文件
LOG_FILE="/var/log/ddns.log"
# --- 全局变量 ---
declare -A ZONE_ID_CACHE
# --- 基础函数 ---
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE"
}
send_telegram_notification(){
local message="$1"
if [[ -n "$Telegram_Bot_Token" && -n "$Telegram_Chat_ID" ]]; then
log "正在发送Telegram通知..."
curl -s --connect-timeout 10 -X POST "https://api.telegram.org/bot$Telegram_Bot_Token/sendMessage" \
-d chat_id="$Telegram_Chat_ID" -d text="$message" >> "$LOG_FILE" 2>&1
else
log "Telegram通知未配置或配置不完整跳过发送。"
fi
}
# --- Cloudflare API 函数 ---
get_root_domain() {
echo "$1" | awk -F. '{print $(NF-1)"."$NF}'
}
get_zone_id() {
local full_domain=$1
local root_domain
root_domain=$(get_root_domain "$full_domain")
if [[ -n "${ZONE_ID_CACHE[$root_domain]}" ]]; then
log "从缓存命中 Zone ID for $root_domain: ${ZONE_ID_CACHE[$root_domain]}"
echo "${ZONE_ID_CACHE[$root_domain]}"
return
fi
log "通过API获取 Zone ID for $root_domain..."
local zone_id_val
ZONE_ID_RESPONSE=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=$root_domain" \
-H "X-Auth-Email: $Email" \
-H "X-Auth-Key: $Api_key" \
-H "Content-Type: application/json")
zone_id_val=$(echo "$ZONE_ID_RESPONSE" | jq -r '.result[] | select(.name=="'"$root_domain"'") | .id' 2>/dev/null)
if [ -z "$zone_id_val" ]; then
log "错误: 无法获取 Zone ID for '$root_domain'. 检查邮箱、API Key或根域名。API响应: $ZONE_ID_RESPONSE"
send_telegram_notification "DDNS 错误: 无法获取 ${root_domain} 的 Cloudflare Zone ID。"
echo ""
else
log "获取成功! Zone ID for $root_domain: $zone_id_val. 已缓存。"
ZONE_ID_CACHE["$root_domain"]="$zone_id_val"
echo "$zone_id_val"
fi
}
get_dns_record_id() {
local zone_id=$1
local record_type=$2
local domain_name=$3
log "尝试获取 DNS Record ID for $domain_name (Type $record_type)..."
DNS_ID_RESPONSE=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records?type=$record_type&name=$domain_name" \
-H "X-Auth-Email: $Email" \
-H "X-Auth-Key: $Api_key" \
-H "Content-Type: application/json")
local dns_id_val
dns_id_val=$(echo "$DNS_ID_RESPONSE" | jq -r '.result[0].id' 2>/dev/null)
if [ -z "$dns_id_val" ]; then
log "警告: 无法获取 '$domain_name' 的 $record_type 记录 ID. 请确保该记录已存在。API响应: $DNS_ID_RESPONSE"
echo ""
else
log "DNS Record ID for $domain_name: $dns_id_val"
echo "$dns_id_val"
fi
}
update_dns_record() {
local record_type=$1
local domain=$2
local public_ip=$3
local old_ip=$4
local old_ip_file=$5
if [[ "$public_ip" == "$old_ip" ]]; then
log "$record_type 地址 ($public_ip) 未变化。"
return 0
fi
log "检测到 $record_type 地址变化: 旧[$old_ip] -> 新[$public_ip]"
local zone_id
zone_id=$(get_zone_id "$domain")
if [ -z "$zone_id" ]; then
log "错误: 因无法获取Zone ID跳过 $domain 的更新。"
return 1
fi
local dns_id
dns_id=$(get_dns_record_id "$zone_id" "$record_type" "$domain")
if [ -z "$dns_id" ]; then
log "错误: 因无法获取DNS Record ID跳过 $domain 的更新。"
send_telegram_notification "DDNS 错误: 更新 ${domain} ($record_type) 失败,无法获取 DNS Record ID。"
return 1
fi
log "正在更新 $domain ($record_type) -> $public_ip..."
response=$(curl -s -w "%{http_code}" -X PUT "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records/$dns_id" \
-H "X-Auth-Email: $Email" \
-H "X-Auth-Key: $Api_key" \
-H "Content-Type: application/json" \
--data "{\"type\":\"$record_type\",\"name\":\"$domain\",\"content\":\"$public_ip\",\"ttl\":60,\"proxied\":false}")
http_code=${response: -3}
body=${response::-3}
if [ "$http_code" -eq 200 ] && [[ "$body" == *"\"success\":true"* ]]; then
log "成功: $domain 的 $record_type 记录已更新为 $public_ip"
echo "$public_ip" > "$old_ip_file"
echo "${domain} 的 ${record_type} 地址已更新为 ${public_ip}。旧IP为 ${old_ip}。"
return 0
else
log "失败: 更新 $domain 失败。HTTP Code: $http_code, Response: $body"
send_telegram_notification "DDNS 错误: 更新 ${domain} ($record_type) 失败. HTTP Code: ${http_code}."
return 1
fi
}
# ==================== 主逻辑开始 ====================
log "====== DDNS 任务开始 ======"
cd "$WORK_DIR" || { log "错误: 无法进入DDNS工作目录 $WORK_DIR"; exit 1; }
source .config
# --- 获取并验证公网IP地址 (已整合老版本验证逻辑) ---
ipv4Regex="^([0-9]{1,3}\.){3}[0-9]{1,3}$"
ipv6Regex="^([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])$"
log "正在从多个源获取公网IP地址..."
raw_ipv4=$(curl -s4 --max-time 10 https://api.ipify.org || curl -s4 --max-time 10 https://ip.sb || curl -s4 --max-time 10 https://ipv4.icanhazip.com)
raw_ipv6=$(curl -s6 --max-time 10 https://api6.ipify.org || curl -s6 --max-time 10 https://ip.sb || curl -s6 --max-time 10 https://ipv6.icanhazip.com)
Public_IPv4=""
if [[ -n "$raw_ipv4" && "$raw_ipv4" =~ $ipv4Regex ]]; then
Public_IPv4="$raw_ipv4"
log "成功获取并验证公网 IPv4: $Public_IPv4"
else
log "警告: 获取到的IPv4内容不是有效地址: '$raw_ipv4'。将忽略IPv4更新。"
fi
Public_IPv6=""
if [[ -n "$raw_ipv6" && "$raw_ipv6" =~ $ipv6Regex ]]; then
Public_IPv6="$raw_ipv6"
log "成功获取并验证公网 IPv6: $Public_IPv6"
else
log "警告: 获取到的IPv6内容不是有效地址: '$raw_ipv6'。将忽略IPv6更新。"
fi
# --- 读取旧的 IP 地址 ---
Old_Public_IPv4=$(cat "$WORK_DIR/.old_ipv4" 2>/dev/null)
Old_Public_IPv6=$(cat "$WORK_DIR/.old_ipv6" 2>/dev/null)
log "旧IPv4: [$Old_Public_IPv4], 旧IPv6: [$Old_Public_IPv6]"
# --- 依次处理IPv4和IPv6的更新 ---
notification_message=""
update_result=""
if [[ -n "$Domain" && "$Domain" != "your_domain.com" && -n "$Public_IPv4" ]]; then
update_result=$(update_dns_record "A" "$Domain" "$Public_IPv4" "$Old_Public_IPv4" "$WORK_DIR/.old_ipv4")
if [[ $? -eq 0 && -n "$update_result" ]]; then
notification_message+="$update_result "
fi
else
log "跳过 IPv4 更新未配置域名或未获取到有效的公网IP。"
fi
if [[ -n "$Domainv6" && "$Domainv6" != "your_domainv6.com" && -n "$Public_IPv6" ]]; then
update_result=$(update_dns_record "AAAA" "$Domainv6" "$Public_IPv6" "$Old_Public_IPv6" "$WORK_DIR/.old_ipv6")
if [[ $? -eq 0 && -n "$update_result" ]]; then
notification_message+="$update_result "
fi
else
log "跳过 IPv6 更新未配置域名或未获取到有效的公网IP。"
fi
# --- 任务结束,发送汇总通知 ---
if [ -n "$notification_message" ]; then
log "IP发生变化准备发送最终通知。"
send_telegram_notification "$notification_message"
else
log "所有IP均未变化或无需更新不发送通知。"
fi
log "====== DDNS 任务结束 ======"
EOF
chmod 700 /etc/DDNS/DDNS
touch /var/log/ddns.log && chmod 644 /var/log/ddns.log
echo -e "${Info}DDNS 文件安装完成!"
echo -e "${Tip}核心逻辑脚本位于: ${YELLOW}/etc/DDNS/DDNS${NC}"
echo -e "${Tip}配置文件位于: ${YELLOW}/etc/DDNS/.config (权限600)${NC}"
echo -e "${Tip}IP状态文件权限已设置为 600。"
echo -e "${Tip}日志文件位于: ${YELLOW}/var/log/ddns.log${NC}"
echo
}
# --- 管理菜单与功能 ---
check_ddns_status(){
if systemctl is-active --quiet ddns.timer; then
ddns_status="running"
else
ddns_status="dead"
fi
}
test_telegram_notification(){
echo -e "${Tip}正在测试 Telegram 通知...${NC}"
source /etc/DDNS/.config
if [[ -z "$Telegram_Bot_Token" || -z "$Telegram_Chat_ID" ]]; then
echo -e "${Error}Telegram Bot Token 或 Chat ID 未配置。请先通过选项 ${GREEN}5${NC} 进行配置。${NC}"
return 1
fi
current_ipv4=$(curl -s4 --max-time 5 https://api.ipify.org || echo "N/A")
local test_message="DDNS (最终修正版) 测试通知。服务器IP: ${current_ipv4}。如果能收到此消息说明Telegram通知功能正常。"
echo -e "${Info}尝试发送测试消息...详情请查看 /var/log/ddns.log"
(
Telegram_Bot_Token="$Telegram_Bot_Token"
Telegram_Chat_ID="$Telegram_Chat_ID"
LOG_FILE="/var/log/ddns.log"
source /etc/DDNS/DDNS
send_telegram_notification "$test_message"
)
echo -e "${Info}测试消息已发送(或尝试发送)。请查看 Telegram 和 ${YELLOW}/var/log/ddns.log${NC} 确认结果。"
}
go_ahead(){
echo -e "${Tip}请选择一个操作:
${GREEN}1${NC}:启动 / 重启 DDNS
${GREEN}2${NC}:停止 DDNS
${GREEN}3${NC}:修改要解析的域名
${GREEN}4${NC}:修改 Cloudflare API
${GREEN}5${NC}:配置 Telegram 通知
${GREEN}6${NC}${RED}彻底卸载 DDNS${NC}
${GREEN}7${NC}:查看 DDNS 实时日志
${GREEN}8${NC}:测试 Telegram 通知
${GREEN}9${NC}立即手动执行一次DDNS检查
${GREEN}0${NC}:退出脚本"
echo
read -p "请输入选项 [0-9]: " option
case "$option" in
0) exit 0 ;;
1) restart_ddns; main ;;
2) stop_ddns; main ;;
3) set_domain; restart_ddns; sleep 2; main ;;
4) set_cloudflare_api; restart_ddns; sleep 2; main ;;
5) set_telegram_settings; restart_ddns; sleep 2; main ;;
6) uninstall_ddns ;;
7) echo -e "${Info}正在显示实时日志... 按 ${RED}Ctrl+C${NC} 退出。"; tail -f /var/log/ddns.log; main ;;
8) test_telegram_notification; sleep 2; main ;;
9) echo -e "${Info}正在手动触发一次DDNS检查..."; systemctl start ddns.service; echo -e "${Info}已触发,请通过日志查看结果。"; sleep 2; main ;;
*) echo -e "${Error}无效的输入!"; sleep 2; main ;;
esac
}
uninstall_ddns(){
read -p "你确定要彻底卸载 DDNS 吗? 这会删除所有配置和脚本。[y/N]: " confirm
if [[ ! "${confirm,,}" =~ ^y$ ]]; then
echo -e "${Info}操作已取消。"
main
return
fi
echo -e "${Tip}正在停止并禁用 systemd 服务..."
systemctl disable --now ddns.service ddns.timer >/dev/null 2>&1
echo -e "${Tip}正在删除相关文件..."
rm -f /etc/systemd/system/ddns.service /etc/systemd/system/ddns.timer
rm -rf /etc/DDNS /usr/bin/ddns
rm -f /var/log/ddns.log
systemctl daemon-reload
echo -e "${Info}DDNS 已成功卸载!"
echo
}
set_cloudflare_api(){
echo -e "${Tip}开始配置 Cloudflare API..." && echo
read -p "请输入您的 Cloudflare 邮箱: " email
read -p "请输入您的 Cloudflare Global API Key: " api_key
if [ -z "$email" ] || [ -z "$api_key" ]; then
echo -e "${Error}邮箱和 API Key 不能为空!"
return 1
fi
sed -i "s/^Email=.*/Email=\"$email\"/" /etc/DDNS/.config
sed -i "s/^Api_key=.*/Api_key=\"$api_key\"/" /etc/DDNS/.config
echo -e "${Info}Cloudflare API 设置已更新!"
}
set_domain(){
echo && echo -e "${Tip}开始配置要解析的域名..."
echo -e "${Tip}请输入要解析的 IPv4 域名 (如: v4.yourdomain.com, 回车跳过):"
read -p "IPv4 域名: " domain_v4
sed -i "s/^Domain=.*/Domain=\"$domain_v4\"/" /etc/DDNS/.config
echo -e "${Tip}请输入要解析的 IPv6 域名 (如: v6.yourdomain.com, 回车跳过):"
read -p "IPv6 域名: " domain_v6
sed -i "s/^Domainv6=.*/Domainv6=\"$domain_v6\"/" /etc/DDNS/.config
echo -e "${Info}域名设置已更新!"
}
set_telegram_settings(){
echo && echo -e "${Tip}开始配置 Telegram 通知 (全部留空则禁用)..."
read -p "请输入您的 Telegram Bot Token: " token
read -p "请输入您的 Telegram Chat ID: " chat_id
sed -i "s/^Telegram_Bot_Token=.*/Telegram_Bot_Token=\"$token\"/" /etc/DDNS/.config
sed -i "s/^Telegram_Chat_ID=.*/Telegram_Chat_ID=\"$chat_id\"/" /etc/DDNS/.config
echo -e "${Info}Telegram 设置已更新!"
}
run_ddns(){
if [ ! -f "/etc/systemd/system/ddns.service" ]; then
cat > /etc/systemd/system/ddns.service <<EOF
[Unit]
Description=Dynamic DNS Update Service (Cloudflare)
After=network.target nss-lookup.target
[Service]
Type=simple
ExecStart=/bin/bash /etc/DDNS/DDNS
WorkingDirectory=/etc/DDNS
[Install]
WantedBy=multi-user.target
EOF
fi
if [ ! -f "/etc/systemd/system/ddns.timer" ]; then
cat > /etc/systemd/system/ddns.timer <<EOF
[Unit]
Description=Run DDNS job every 5 minutes
[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
Unit=ddns.service
[Install]
WantedBy=timers.target
EOF
fi
systemctl daemon-reload
restart_ddns
}
restart_ddns(){
echo -e "${Info}正在启动/重启 DDNS 定时任务..."
systemctl start ddns.service
systemctl enable --now ddns.timer >/dev/null 2>&1
echo -e "${Info}DDNS 服务已启动。将每5分钟检查一次IP变更。"
}
stop_ddns(){
echo -e "${Info}正在停止 DDNS 定时任务..."
systemctl disable --now ddns.timer >/dev/null 2>&1
systemctl stop ddns.service >/dev/null 2>&1
echo -e "${Info}DDNS 已停止。"
}
main(){
if [[ -z "$IS_RECURSIVE" ]]; then
cop_info
fi
if [ ! -f "/etc/DDNS/.config" ] || [ ! -f "/usr/bin/ddns" ]; then
echo -e "${Tip}首次运行,开始安装流程..."
install_ddns
set_cloudflare_api
set_domain
set_telegram_settings
run_ddns
echo
echo -e "${GREEN_ground} DDNS (最终修正版) 安装并配置成功! ${NC}"
echo -e "${Info}你可以随时再次运行此脚本或直接输入 ${GREEN}ddns${NC} 进行管理。"
echo -e "${Info}重要:请通过选项 ${GREEN}7${NC} 查看日志以确认首次运行是否成功。"
echo
else
check_ddns_status
if [[ "$ddns_status" == "running" ]]; then
echo -e "${Info}DDNS 状态: ${GREEN}已安装${NC} | ${GREEN}运行中${NC}"
else
echo -e "${Info}DDNS 状态: ${GREEN}已安装${NC} | ${RED}已停止${NC}"
fi
echo
fi
export IS_RECURSIVE=true
go_ahead
}
# --- 脚本执行起点 ---
check_root
check_curl
main