Cloudflare DDNS shell 脚本使用

Cloudflare DDNS shell 脚本使用

功能介绍

这是一个自动更新 Cloudflare DNS 记录的脚本,支持 IPv4 (A记录) 和 IPv6 (AAAA记录) 的动态DNS更新。

文件结构

ddns.sh      # 主脚本文件
ddns.conf    # 配置文件

配置文件格式

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

# Cloudflare API Token
CF_API_TOKEN="你的API Token"

# 域名配置
DOMAIN="example.com"        # 主域名
SUBDOMAIN="home"           # 子域名

# 记录类型: A 或 AAAA
RECORD_TYPE="A"

获取 Cloudflare API Token

  1. 登录 Cloudflare 控制台
  2. 进入 我的个人资料 → API 令牌
  3. 点击 创建令牌
  4. 选择 自定义令牌 模板
  5. 设置权限:
    • 区域 – Zone:Read
    • DNS – Zone:Edit
  6. 区域资源选择你的域名
  7. 创建并复制 Token

使用方法

基本使用

chmod +x ddns.sh
./ddns.sh ddns.conf

定时任务

编辑 crontab:

crontab -e

添加定时任务(每5分钟检查一次):

*/5 * * * * /path/to/ddns.sh /path/to/ddns.conf >> /var/log/ddns.log 2>&1

支持的系统

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

依赖包

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

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

输出示例

成功更新

正在获取当前出口IP...
当前出口IP: 192.168.1.100
正在查询DNS记录: home.example.com (A)
通过dig获取到DNS IP: 192.168.1.99
IP不一致,需要更新: DNS=192.168.1.99, 当前=192.168.1.100
正在获取Cloudflare Zone ID...
Zone ID: abc123...
正在查询现有DNS记录...
记录存在,Record ID: def456...,更新中...
更新成功: home.example.com -> 192.168.1.100

无需更新

正在获取当前出口IP...
当前出口IP: 192.168.1.100
正在查询DNS记录: home.example.com (A)
通过dig获取到DNS IP: 192.168.1.100
出口 IP 与 DNS 一致,无需更新 (192.168.1.100)

配置说明

参数 说明 示例
CF_API_TOKEN Cloudflare API Token abc123def456...
DOMAIN 主域名 example.com
SUBDOMAIN 子域名 home
RECORD_TYPE 记录类型 A 或 AAAA

最终更新的域名为:SUBDOMAIN.DOMAIN

常见问题

1. 获取IP失败

  • 检查网络连接
  • 确认可以访问 https://v4.ipgg.cn/ip 或 https://v6.ipgg.cn/ip

2. API调用失败

  • 检查 CF_API_TOKEN 是否正确
  • 确认 Token 权限包含域名的 Zone:Read 和 Zone:Edit

3. DNS查询失败

  • 确认域名存在
  • 检查 dig 或 nslookup 命令是否可用

4. 权限错误

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

日志记录

在 crontab 中设置日志:

*/5 * * * * /path/to/ddns.sh /path/to/ddns.conf >> /var/log/ddns.log 2>&1

DDNS脚本内容

#!/bin/bash

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" || 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 "$1"
    elif command -v opkg >/dev/null 2>&1; then
        opkg update && 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=$(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

[ -z "$CF_API_TOKEN" ] && echo "CF_API_TOKEN 未设置" && exit 1
[ -z "$DOMAIN" ] && echo "DOMAIN 未设置" && exit 1
[ -z "$SUBDOMAIN" ] && echo "SUBDOMAIN 未设置" && exit 1
[ -z "$RECORD_TYPE" ] && echo "RECORD_TYPE 未设置" && exit 1

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

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")

if [ -n "$DNS_IP" ]; then
    if [ "$CURRENT_IP" == "$DNS_IP" ]; then
        echo "出口 IP 与 DNS 一致,无需更新 ($CURRENT_IP)"
        exit 0
    else
        echo "IP不一致,需要更新: DNS=$DNS_IP, 当前=$CURRENT_IP"
    fi
else
    echo "DNS记录不存在或查询失败,将创建新记录"
fi

echo "正在获取Cloudflare Zone ID..."
zone_json=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=$DOMAIN" \
    -H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json")
ZONE_ID=$(echo "$zone_json" | jq -r '.result[0].id')
[ -z "$ZONE_ID" ] || [ "$ZONE_ID" == "null" ] && echo "无法获取 Zone ID" && exit 1
echo "Zone ID: $ZONE_ID"

echo "正在查询现有DNS记录..."
record_json=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?name=$record_name&type=$RECORD_TYPE" \
    -H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json")
RECORD_ID=$(echo "$record_json" | jq -r '.result[0].id')

if [ -z "$RECORD_ID" ] || [ "$RECORD_ID" == "null" ]; then
    echo "记录不存在,创建中..."
    response=$(curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" \
        -H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" \
        --data "{\"type\":\"$RECORD_TYPE\",\"name\":\"$record_name\",\"content\":\"$CURRENT_IP\",\"ttl\":600,\"proxied\":false}")
else
    echo "记录存在,Record ID: $RECORD_ID,更新中..."
    response=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$RECORD_ID" \
        -H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" \
        --data "{\"type\":\"$RECORD_TYPE\",\"name\":\"$record_name\",\"content\":\"$CURRENT_IP\",\"ttl\":600,\"proxied\":false}")
fi

echo "$response" | grep -q '"success":true' && echo "更新成功: $record_name -> $CURRENT_IP" || {
    echo "更新失败"
    echo "$response"
    exit 1
}
© 版权声明
THE END
喜欢就支持一下吧
点赞14 分享