吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 523|回复: 2
收起左侧

[其他原创] PVE下借助QGA对虚拟机进行DDNS

  [复制链接]
痴情总被无情伤 发表于 2025-3-22 05:46

PVE下借助QGA对虚拟机进行DDNS

pve 平台对虚拟机进行 DDNS 的方案有很多,今天花了点时间自己写了一个。

适用于提取安装了 QGA(Qemu-Guest-Agent)服务的虚拟机

功能实现的核心是以下几行命令

# 通过qm命令查询指定虚拟机的mac地址
qm config "$vmid"
# 通过qga查询指定虚拟机的主机名
qm guest cmd "$vmid" get-host-name
# 通过qga查询指定虚拟机的网卡接口信息
qm guest cmd "$vmid" network-get-interfaces
# 通过qm命令获取虚拟机列表
qm list
# 通过qga执行命令
qm guest exec "$vmid" ip a

优化一下可以得到这些工具函数

# 获取虚拟机信息 Mac地址 hostname
get_vm_info() {
    local vmid=$1
    local mac=$(qm config "$vmid" | awk -F= '/virtio/ {print $2}' | grep -oE '([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}' | head -1 | tr '[:upper:]' '[:lower:]')
    local hostname=$(qm guest cmd "$vmid" get-host-name 2>/dev/null | jq -r '."host-name"')

    echo "$mac ${hostname:-vm-$vmid}"
}

# 获取IP地址
get_ip_address() {
    local vmid=$1
    local mac=$2

    local network_data=$(qm guest cmd "$vmid" network-get-interfaces 2>/dev/null)
    [[ -z "$network_data" ]] && return 1

    local ipv4=$(echo "$network_data" | jq -r --arg mac "$mac" '
        .[] | select(.["hardware-address"] == $mac) |
        .["ip-addresses"][] | select(.["ip-address-type"] == "ipv4") |
        .["ip-address"]' | head -1)

    local ipv6=$(echo "$network_data" | jq -r --arg mac "$mac" '
        .[] | select(.["hardware-address"] == $mac) |
        .["ip-addresses"][] | select(.["ip-address-type"] == "ipv6") |
        .["ip-address"]' | awk '/^240/' | head -1)
    # awk '/^240/' 过滤 240开头的ipv6公网

    echo "$ipv4 $ipv6"
}

整合一下 dns 运营商通过的 api 就可以实现 DDNS 了

# DNSPod API实现
update_dnspod_dns() {
    local subdomain=$1 type=$2 ip=$3
    local api_url="https://dnsapi.cn/Record.Ddns"

    # 获取记录ID
    local record_info=$(curl -sX POST $api_url \
        -d "login_token=$DNSPOD_API_TOKEN&format=json&domain=$DOMAIN&sub_domain=$subdomain&record_type=$type")
    local record_id=$(jq -r '.record.id' <<<"$record_info")
    local code=$(jq -r '.status.code' <<<"$record_info")

    # 更新或创建记录
    if [[ "$code" == "1" && -n "$record_id" ]]; then
        echo "DNSPod记录更新:${subdomain}.${DOMAIN}$ip"
    else
        response=$(curl -sX POST "https://dnsapi.cn/Record.Create" \
            -d "login_token=$DNSPOD_API_TOKEN&format=json&domain=$DOMAIN&sub_domain=$subdomain&record_type=$type&record_line=默认&value=$ip")
        local message=$(jq -r '.status.message' <<<"$response")
        echo "DNSPod记录创建:${subdomain}.${DOMAIN}$ip $message" || return 1
    fi
}

主函数


    for vmid in $(qm list | awk '$3 == "running" {print $1}'); do
        echo "处理虚拟机: $vmid"
        read mac hostname <<< $(get_vm_info "$vmid")
        [[ -z "$mac" ]] && continue

        read ipv4 ipv6 <<< $(get_ip_address "$vmid" "$mac")
        local rand_id=$(generate_rand_id "$vmid")

        if [[ $ENABLE_IPV4 ]];then
            if [[ $ENABLE_QUERY_PUBLIC_IPV4 ]];then
                ipv4=$(curl -s https://myip.ipip.net | grep -oP '当前 IP:\K[0-9.]+')
            fi
        fi

        [[ -n "$ipv4" ]] && {
            if [[ $ENABLE_IPV4 ]];then
                if [[ $ENABLE_QUERY_PUBLIC_IPV4 ]];then
                    ipv4=$(curl -s https://myip.ipip.net | grep -oP '当前 IP:\K[0-9.]+')
                fi
            local subdomain="ipv4.${hostname}${rand_id}"
            case "$DNS_PROVIDER" in
                dnspod)    update_dnspod_dns "$subdomain" "A" "$ipv4" ;;
                cloudflare) update_cloudflare_dns "$subdomain" "A" "$ipv4" ;;
                aliyun)    update_aliyun_dns "$subdomain" "A" "$ipv4" ;;
            esac
            fi
        }

        [[ -n "$ipv6" ]] && {
            if [[ $ENABLE_IPV6 ]];then
            local subdomain="ipv6.${hostname}${rand_id}"
            case "$DNS_PROVIDER" in
                dnspod)    update_dnspod_dns "$subdomain" "AAAA" "$ipv6" ;;
                cloudflare) update_cloudflare_dns "$subdomain" "AAAA" "$ipv6" ;;
                aliyun)    update_aliyun_dns "$subdomain" "AAAA" "$ipv6" ;;
            esac
        fi
        }
    done
    echo "DDNS更新完成"

下面给出完整代码,我测试 DNSPOD 是可以正常使用的

#!/bin/bash
# DDNS自动更新脚本(支持DNSPod/Cloudflare/阿里云)
# 通过QGA代理获取虚拟机的MAC地址 HostName IPV4/6地址配置ddns
# DDNS设置的是[ipv4 or ipv6].[hostname][4长度随机字符].YourDoman
# 适用于安装了QGA的虚拟机
# 如果使用crontab的话记得加环境变量,还有,使用crontab执行的话脚本的数据目录要写绝对路径,否则每次添加的都将是新域名
# PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# SHELL=/bin/bash
# */10 * * * * source /root/.bashrc && /root/ddns/pve-vm-ddns.sh
############### 功能配置区域 ###############
ENABLE_IPV4=true                                    # 开启IPV4DDNS
ENABLE_QUERY_PUBLIC_IPV4=true                       # 通过PUBLIC_IPV4_QUERY_LINK获得IPV4公网地址
ENABLE_IPV6=true                                    # 开启IPV6 DDNS
PUBLIC_IPV4_QUERY_LINK=https://myip.ipip.net        # 获取公网ipv4
############### 用户配置区域 ###############
DOMAIN="yourdomain.com"             # 主域名
DATA_DIR="/root/ddns/data"                   # 数据存储目录
# 这里我的域名只有DNSPOD的,只测试了dnspod的,其他服务商需要自行完善
DNS_PROVIDER="dnspod"               # 可选:dnspod/cloudflare/aliyun

# DNSPod配置(https://docs.dnspod.cn/account/5f2d466de8320f1a740d9ff3/)
DNSPOD_API_TOKEN="ID,token"         # 格式:API_ID,API_Token

# Cloudflare配置(https://dash.cloudflare.com/profile/api-tokens)
CLOUDFLARE_API_TOKEN=""             # API Token需要Zone:DNS:Edit权限
CLOUDFLARE_ZONE_ID=""               # 在域概述页面右侧获取

# 阿里云配置
ALIYUN_ACCESS_KEY=""
ALIYUN_SECRET_KEY=""
###########################################

# 初始化日志
LOG_FILE="/root/ddns/ddns.log"
log() {
    local level="$1"; shift
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    echo "[$timestamp] [$level] $*" | tee -a "$LOG_FILE"
}

# 依赖检查
declare -A DEPENDENCIES=([jq]="apt install jq" [curl]="apt install curl")
check_dependencies() {
    for cmd in "${!DEPENDENCIES[@]}"; do
        if ! command -v "$cmd" &>/dev/null; then
            echo "错误:缺少必要组件 $cmd,安装方法:${DEPENDENCIES[$cmd]}"
            exit 1
        fi
    done
}

# 生成随机标识
generate_rand_id() {
    local vmid=$1
    local rand_file="$DATA_DIR/$vmid.rand"
    [[ ! -f "$rand_file" ]] && openssl rand -hex 2 > "$rand_file"
    cat "$rand_file"
}

# 获取虚拟机信息 Mac地址 hostname
get_vm_info() {
    local vmid=$1
    local mac=$(qm config "$vmid" | awk -F= '/virtio/ {print $2}' | grep -oE '([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}' | head -1 | tr '[:upper:]' '[:lower:]')
    local hostname=$(qm guest cmd "$vmid" get-host-name 2>/dev/null | jq -r '."host-name"')

    echo "$mac ${hostname:-vm-$vmid}"
}

# 获取IP地址
get_ip_address() {
    local vmid=$1
    local mac=$2

    local network_data=$(qm guest cmd "$vmid" network-get-interfaces 2>/dev/null)
    [[ -z "$network_data" ]] && return 1

    local ipv4=$(echo "$network_data" | jq -r --arg mac "$mac" '
        .[] | select(.["hardware-address"] == $mac) |
        .["ip-addresses"][] | select(.["ip-address-type"] == "ipv4") |
        .["ip-address"]' | head -1)

    local ipv6=$(echo "$network_data" | jq -r --arg mac "$mac" '
        .[] | select(.["hardware-address"] == $mac) |
        .["ip-addresses"][] | select(.["ip-address-type"] == "ipv6") |
        .["ip-address"]' | awk '/^240/' | head -1)
    # awk '/^240/' 过滤 240开头的ipv6公网

    echo "$ipv4 $ipv6"
}

################ DNS服务商实现 ################
# DNSPod API实现
update_dnspod_dns() {
    local subdomain=$1 type=$2 ip=$3
    local api_url="https://dnsapi.cn/Record.Ddns"

    # 获取记录ID
    local record_info=$(curl -sX POST $api_url \
        -d "login_token=$DNSPOD_API_TOKEN&format=json&domain=$DOMAIN&sub_domain=$subdomain&record_type=$type")
    local record_id=$(jq -r '.record.id' <<<"$record_info")
    local code=$(jq -r '.status.code' <<<"$record_info")

    # 更新或创建记录
    if [[ "$code" == "1" && -n "$record_id" ]]; then
        echo "DNSPod记录更新:${subdomain}.${DOMAIN}$ip"
    else
        response=$(curl -sX POST "https://dnsapi.cn/Record.Create" \
            -d "login_token=$DNSPOD_API_TOKEN&format=json&domain=$DOMAIN&sub_domain=$subdomain&record_type=$type&record_line=默认&value=$ip")
        handle_dns_response "dnspod" "$type" "$subdomain" "$ip" "$response" || return 1
    fi
}

# Cloudflare API实现
update_cloudflare_dns() {
    local subdomain=$1 type=$2 ip=$3
    local record_name="${subdomain}.$DOMAIN"
    local api_url="https://api.cloudflare.com/client/v4/zones/$CLOUDFLARE_ZONE_ID/dns_records"

    # 查找现有记录
    local response=$(curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
        -H "Content-Type: application/json" \
        "$api_url?name=$record_name&type=$type")

    local record_id=$(jq -r '.result[0].id' <<<"$response")

    # 执行更新/创建
    if [[ "$record_id" != "null" && -n "$record_id" ]]; then
        response=$(curl -sX PUT "$api_url/$record_id" \
            -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
            -H "Content-Type: application/json" \
            -d "{\"type\":\"$type\",\"name\":\"$subdomain\",\"content\":\"$ip\",\"ttl\":600}")
    else
        response=$(curl -sX POST "$api_url" \
            -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
            -H "Content-Type: application/json" \
            -d "{\"type\":\"$type\",\"name\":\"$subdomain\",\"content\":\"$ip\",\"ttl\":600,\"proxied\":false}")
    fi

    handle_dns_response "cloudflare" "$type" "$subdomain" "$ip" "$response" || return 1
}

# 阿里云API实现
update_aliyun_dns() {
    local subdomain=$1 type=$2 ip=$3
    local timestamp=$(date -u +%Y-%m-%dT%TZ)
    local params="AccessKeyId=$ALIYUN_ACCESS_KEY&Action=DescribeDomainRecords&DomainName=$DOMAIN&Format=JSON&RRKeyWord=$subdomain&SignatureMethod=HMAC-SHA1&SignatureNonce=$RANDOM&SignatureVersion=1.0&Timestamp=${timestamp}&Type=$type&Version=2015-01-09"
    local signature=$(aliyun_sign "$ALIYUN_SECRET_KEY" "$params")

    local response=$(curl -s "http://alidns.aliyuncs.com/?$params&Signature=$(echo -n "$signature" | curl -Gso /dev/null -w %{url_effective} --data-urlencode @- "" | cut -c3-)")
    local record_id=$(jq -r '.DomainRecords.Record[0].RecordId' <<<"$response")
    local current_ip=$(jq -r '.DomainRecords.Record[0].Value' <<<"$response")

    if [[ "$record_id" != "null" && -n "$record_id" ]]; then
        [[ "$current_ip" == "$ip" ]] && return
        params="Action=UpdateDomainRecord&RecordId=$record_id&RR=$subdomain&SignatureMethod=HMAC-SHA1&SignatureNonce=$RANDOM&SignatureVersion=1.0&Timestamp=${timestamp}&Type=$type&Value=$ip&Version=2015-01-09"
    else
        params="Action=AddDomainRecord&DomainName=$DOMAIN&RR=$subdomain&SignatureMethod=HMAC-SHA1&SignatureNonce=$RANDOM&SignatureVersion=1.0&Timestamp=${timestamp}&Type=$type&Value=$ip&Version=2015-01-09"
    fi

    signature=$(aliyun_sign "$ALIYUN_SECRET_KEY" "$params")
    response=$(curl -s "http://alidns.aliyuncs.com/?$params&Signature=$(echo -n "$signature" | curl -Gso /dev/null -w %{url_effective} --data-urlencode @- "" | cut -c3-)" | jq . &>/dev/null)
    handle_dns_response "aliyun" "$type" "$subdomain" "$ip" "$response" || return 1
}

format_log() {
    local provider=$1 status=$2 type=$3 subdomain=$4 ip=$5 extra=$6
    printf "[%-8s] %-6s 类型:%-4s 记录:%-40s → %-25s %s\n" \
        "$provider" "$status" "$type" "${subdomain}.${DOMAIN}" "$ip" "$extra"
    log "$status" "$provider | $type | $subdomain.$DOMAIN$ip $extra"
}

# 通用响应处理函数
handle_dns_response() {
    local provider=$1 type=$2 subdomain=$3 ip=$4 response=$5
    case "$provider" in
    dnspod)
        local code=$(jq -r '.status.code' <<<"$response")
        local message=$(jq -r '.status.message' <<<"$response")
        local record_id=$(jq -r '.record.id' <<<"$response")

        if [[ "$code" == "1" ]]; then
            if [[ -n "$record_id" && "$record_id" != "0" ]]; then
                format_log "DNSPod" "成功" "$type" "$subdomain" "$ip" "(ID:$record_id)"
            else
                format_log "DNSPod" "创建" "$type" "$subdomain" "$ip"
            fi
        elif [[ "$code" == "104" ]]; then
            format_log "DNSPod" "记录已存在" "$type" "$subdomain" "$ip" "原因:$message"
            return 1
        elif [[ "$code" == "110" ]]; then
            format_log "DNSPod" "域名没有备案" "$type" "$subdomain" "$ip" "原因:$message"
            return 1
        else
            format_log "DNSPod" "失败" "$type" "$subdomain" "$ip" "Code:$code 原因:$message"
            return 1
        fi
        ;;
    cloudflare)
        local success=$(jq -r '.success' <<<"$response")
        if [[ "$success" == "true" ]]; then
            local record_id=$(jq -r '.result.id' <<<"$response")
            format_log "Cloudflare" "成功" "$type" "$subdomain" "$ip" "(ID:${record_id:0:8}...)"
        else
            local errors=$(jq -r '.errors[].message' <<<"$response" | tr '\n' ' ')
            format_log "Cloudflare" "失败" "$type" "$subdomain" "$ip" "原因:$errors"
            return 1
        fi
        ;;
    aliyun)
        local code=$(jq -r '.Code' <<<"$response")
        if [[ "$code" == "OK" ]]; then
            local record_id=$(jq -r '.RecordId' <<<"$response")
            format_log "Aliyun" "成功" "$type" "$subdomain" "$ip" "(ID:$record_id)"
        else
            local message=$(jq -r '.Message' <<<"$response")
            format_log "Aliyun" "失败" "$type" "$subdomain" "$ip" "原因:$message"
            return 1
        fi
        ;;
    esac
}

# 主处理流程
main() {
    log "INFO" "开始DDNS更新..."
    check_dependencies
    mkdir -p "$DATA_DIR"

    for vmid in $(qm list | awk '$3 == "running" {print $1}'); do
        log "INFO" "处理虚拟机: $vmid"
        read mac hostname <<< $(get_vm_info "$vmid")
        [[ -z "$mac" ]] && continue

        read ipv4 ipv6 <<< $(get_ip_address "$vmid" "$mac")
        local rand_id=$(generate_rand_id "$vmid")

        if [[ $ENABLE_IPV4 ]];then
            if [[ $ENABLE_QUERY_PUBLIC_IPV4 ]];then
                ipv4=$(curl -s https://myip.ipip.net | grep -oP '当前 IP:\K[0-9.]+')
            fi
        fi

        [[ -n "$ipv4" ]] && {
            if [[ $ENABLE_IPV4 ]];then
                if [[ $ENABLE_QUERY_PUBLIC_IPV4 ]];then
                    ipv4=$(curl -s https://myip.ipip.net | grep -oP '当前 IP:\K[0-9.]+')
                fi
            local subdomain="ipv4.${hostname}${rand_id}"
            case "$DNS_PROVIDER" in
                dnspod)    update_dnspod_dns "$subdomain" "A" "$ipv4" ;;
                cloudflare) update_cloudflare_dns "$subdomain" "A" "$ipv4" ;;
                aliyun)    update_aliyun_dns "$subdomain" "A" "$ipv4" ;;
            esac
            fi
        }

        [[ -n "$ipv6" ]] && {
            if [[ $ENABLE_IPV6 ]];then
            local subdomain="ipv6.${hostname}${rand_id}"
            case "$DNS_PROVIDER" in
                dnspod)    update_dnspod_dns "$subdomain" "AAAA" "$ipv6" ;;
                cloudflare) update_cloudflare_dns "$subdomain" "AAAA" "$ipv6" ;;
                aliyun)    update_aliyun_dns "$subdomain" "AAAA" "$ipv6" ;;
            esac
        fi
        }
    done
    log "INFO" "DDNS更新完成"
}

# 执行入口
main


效果

效果

免费评分

参与人数 2吾爱币 +7 热心值 +2 收起 理由
苏紫方璇 + 7 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
heliotrope + 1 我很赞同!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

heliotrope 发表于 2025-3-22 10:25
太厉害了!!收下这赞!
 楼主| 痴情总被无情伤 发表于 2025-3-22 17:20

更新一下LXC容器支持

LXC容器使用以下方法获得ip mac hostname

# 通过pct查询lxc容器的mac地址
pct config "$vmid" | grep -iE 'net0|ip' | grep -oE '([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}' | head -1 | tr '[:upper:]' '[:lower:]'
# 通过pct查询lxc容器的hostname
pct config "$vmid"| grep -iE 'hostname' | awk '{print $2}'
# 通过pct命令在容器内执行命令获取ip
pct exec "$vmid" -- curl -s Ipv6.icanhazip.com
pct exec "$vmid" -- curl -s Ipv4.icanhazip.com

优化一下可以得到这些工具函数

get_lxc_info() {
    local vmid=$1
    local mac=$(pct config "$vmid" | grep -iE 'net0|ip' | grep -oE '([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}' | head -1 | tr '[:upper:]' '[:lower:]')
    local hostname=$(pct config "$vmid"| grep -iE 'hostname' | awk '{print $2}')

    echo "$mac ${hostname:-vm-$vmid}"
}

get_lxc_ip_address() {
    local vmid=$1
    local mac=$2
    local hostname=$3
    local ipv4=$(pct exec "$vmid" -- curl -s Ipv4.icanhazip.com --connect-timeout 2)
    local ipv6=$(pct exec "$vmid" -- curl -s Ipv6.icanhazip.com --connect-timeout 2)

    echo "$ipv4 $ipv6"
}

主函数

    for vmid in $(pct list | awk '$2 == "running" {print $1}'); do
        echo "处理LXC容器: $vmid"
        read mac hostname <<< $(get_lxc_info "$vmid")
        [[ -z "$mac" ]] && continue

        read ipv4 ipv6 <<< $(get_lxc_ip_address "$vmid" "$mac")
        local rand_id=$(generate_rand_id "$vmid")

        [[ -n "$ipv4" ]] && {
            if $ENABLE_IPV4;then
            local subdomain="ipv4.${hostname}${rand_id}"
            case "$DNS_PROVIDER" in
                dnspod)    update_dnspod_dns "$subdomain" "A" "$ipv4" ;;
                cloudflare) update_cloudflare_dns "$subdomain" "A" "$ipv4" ;;
                aliyun)    update_aliyun_dns "$subdomain" "A" "$ipv4" ;;
            esac
            fi
        }

        [[ -n "$ipv6" ]] && {
            if $ENABLE_IPV6;then
            local subdomain="ipv6.${hostname}${rand_id}"
            case "$DNS_PROVIDER" in
                dnspod)    update_dnspod_dns "$subdomain" "AAAA" "$ipv6" ;;
                cloudflare) update_cloudflare_dns "$subdomain" "AAAA" "$ipv6" ;;
                aliyun)    update_aliyun_dns "$subdomain" "AAAA" "$ipv6" ;;
            esac
        fi
        }
    done

完整代码

#!/bin/bash
# DDNS自动更新脚本(支持DNSPod/Cloudflare/阿里云)
# 通过QGA代理获取虚拟机的MAC地址 HostName IPV4/6地址配置ddns
# DDNS设置的是[ipv4 or ipv6].[hostname][4长度随机字符].YourDoman
# 适用于安装了QGA的虚拟机和LXC容器
# 如果使用crontab的话记得加环境变量,还有,使用crontab执行的话脚本的数据目录要写绝对路径,否则每次添加的都将是新域名
# PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# SHELL=/bin/bash
# */10 * * * * source /root/.bashrc && /root/ddns/pve-vm-ddns.sh

# 更新 追加LXC容器支持
# 要求LXC容器需要有curl命令
############### 功能配置区域 ###############
ENABLE_IPV4=false                                    # 开启IPV4DDNS
ENABLE_QUERY_PUBLIC_IPV4=true                       # 通过PUBLIC_IPV4_QUERY_LINK获得IPV4公网地址
ENABLE_IPV6=true                                    # 开启IPV6 DDNS
PUBLIC_IPV4_QUERY_LINK=https://ipv4.icanhazip.com        # 获取公网ipv4
############### 用户配置区域 ###############
DOMAIN="yourdomain.com"             # 主域名
DATA_DIR="/root/ddns/data"                   # 数据存储目录
# 这里我的域名只有DNSPOD的,只测试了dnspod的,其他服务商需要自行完善
DNS_PROVIDER="dnspod"               # 可选:dnspod/cloudflare/aliyun

# DNSPod配置(https://docs.dnspod.cn/account/5f2d466de8320f1a740d9ff3/)
DNSPOD_API_TOKEN="ID,token"         # 格式:API_ID,API_Token

# Cloudflare配置(https://dash.cloudflare.com/profile/api-tokens)
CLOUDFLARE_API_TOKEN=""             # API Token需要Zone:DNS:Edit权限
CLOUDFLARE_ZONE_ID=""               # 在域概述页面右侧获取

# 阿里云配置
ALIYUN_ACCESS_KEY=""
ALIYUN_SECRET_KEY=""
###########################################

# 初始化日志
LOG_FILE="/root/ddns/ddns.log"
log() {
    local level="$1"; shift
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    echo "[$timestamp] [$level] $*" | tee -a "$LOG_FILE"
}

# 依赖检查
declare -A DEPENDENCIES=([jq]="apt install jq" [curl]="apt install curl")
check_dependencies() {
    for cmd in "${!DEPENDENCIES[@]}"; do
        if ! command -v "$cmd" &>/dev/null; then
            echo "错误:缺少必要组件 $cmd,安装方法:${DEPENDENCIES[$cmd]}"
            exit 1
        fi
    done
}

# 生成随机标识
generate_rand_id() {
    local vmid=$1
    local rand_file="$DATA_DIR/$vmid.rand"
    [[ ! -f "$rand_file" ]] && openssl rand -hex 2 > "$rand_file"
    cat "$rand_file"
}

# 获取虚拟机信息 Mac地址 hostname
get_vm_info() {
    local vmid=$1
    local mac=$(qm config "$vmid" | awk -F= '/virtio/ {print $2}' | grep -oE '([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}' | head -1 | tr '[:upper:]' '[:lower:]')
    local hostname=$(qm guest cmd "$vmid" get-host-name 2>/dev/null | jq -r '."host-name"')

    echo "$mac ${hostname:-vm-$vmid}"
}

# 获取IP地址
get_ip_address() {
    local vmid=$1
    local mac=$2

    local network_data=$(qm guest cmd "$vmid" network-get-interfaces 2>/dev/null)
    [[ -z "$network_data" ]] && return 1

    local ipv4=$(echo "$network_data" | jq -r --arg mac "$mac" '
        .[] | select(.["hardware-address"] == $mac) |
        .["ip-addresses"][] | select(.["ip-address-type"] == "ipv4") |
        .["ip-address"]' | head -1)

    local ipv6=$(echo "$network_data" | jq -r --arg mac "$mac" '
        .[] | select(.["hardware-address"] == $mac) |
        .["ip-addresses"][] | select(.["ip-address-type"] == "ipv6") |
        .["ip-address"]' | awk '/^240/' | head -1)
    # awk '/^240/' 过滤 240开头的ipv6公网

    echo "$ipv4 $ipv6"
}

get_lxc_info() {
    local vmid=$1
    local mac=$(pct config "$vmid" | grep -iE 'net0|ip' | grep -oE '([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}' | head -1 | tr '[:upper:]' '[:lower:]')
    local hostname=$(pct config "$vmid"| grep -iE 'hostname' | awk '{print $2}')

    echo "$mac ${hostname:-vm-$vmid}"
}

get_lxc_ip_address() {
    local vmid=$1
    local mac=$2
    local ipv4=$(pct exec "$vmid" -- curl -s Ipv4.icanhazip.com --connect-timeout 2)
    local ipv6=$(pct exec "$vmid" -- curl -s Ipv6.icanhazip.com --connect-timeout 2)

    echo "$ipv4 $ipv6"
}

################ DNS服务商实现 ################
# DNSPod API实现
update_dnspod_dns() {
    local subdomain=$1 type=$2 ip=$3
    local api_url="https://dnsapi.cn/Record.Ddns"

    # 获取记录ID
    local record_info=$(curl -sX POST $api_url \
        -d "login_token=$DNSPOD_API_TOKEN&format=json&domain=$DOMAIN&sub_domain=$subdomain&record_type=$type")
    local record_id=$(jq -r '.record.id' <<<"$record_info")
    local code=$(jq -r '.status.code' <<<"$record_info")

    # 更新或创建记录
    if [[ "$code" == "1" && -n "$record_id" ]]; then
        echo "DNSPod记录更新:${subdomain}.${DOMAIN}$ip"
    else
        response=$(curl -sX POST "https://dnsapi.cn/Record.Create" \
            -d "login_token=$DNSPOD_API_TOKEN&format=json&domain=$DOMAIN&sub_domain=$subdomain&record_type=$type&record_line=默认&value=$ip")
        handle_dns_response "dnspod" "$type" "$subdomain" "$ip" "$response" || return 1
    fi
}

# Cloudflare API实现
update_cloudflare_dns() {
    local subdomain=$1 type=$2 ip=$3
    local record_name="${subdomain}.$DOMAIN"
    local api_url="https://api.cloudflare.com/client/v4/zones/$CLOUDFLARE_ZONE_ID/dns_records"

    # 查找现有记录
    local response=$(curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
        -H "Content-Type: application/json" \
        "$api_url?name=$record_name&type=$type")

    local record_id=$(jq -r '.result[0].id' <<<"$response")

    # 执行更新/创建
    if [[ "$record_id" != "null" && -n "$record_id" ]]; then
        response=$(curl -sX PUT "$api_url/$record_id" \
            -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
            -H "Content-Type: application/json" \
            -d "{\"type\":\"$type\",\"name\":\"$subdomain\",\"content\":\"$ip\",\"ttl\":600}")
    else
        response=$(curl -sX POST "$api_url" \
            -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
            -H "Content-Type: application/json" \
            -d "{\"type\":\"$type\",\"name\":\"$subdomain\",\"content\":\"$ip\",\"ttl\":600,\"proxied\":false}")
    fi

    handle_dns_response "cloudflare" "$type" "$subdomain" "$ip" "$response" || return 1
}

# 阿里云API实现
update_aliyun_dns() {
    local subdomain=$1 type=$2 ip=$3
    local timestamp=$(date -u +%Y-%m-%dT%TZ)
    local params="AccessKeyId=$ALIYUN_ACCESS_KEY&Action=DescribeDomainRecords&DomainName=$DOMAIN&Format=JSON&RRKeyWord=$subdomain&SignatureMethod=HMAC-SHA1&SignatureNonce=$RANDOM&SignatureVersion=1.0&Timestamp=${timestamp}&Type=$type&Version=2015-01-09"
    local signature=$(aliyun_sign "$ALIYUN_SECRET_KEY" "$params")

    local response=$(curl -s "http://alidns.aliyuncs.com/?$params&Signature=$(echo -n "$signature" | curl -Gso /dev/null -w %{url_effective} --data-urlencode @- "" | cut -c3-)")
    local record_id=$(jq -r '.DomainRecords.Record[0].RecordId' <<<"$response")
    local current_ip=$(jq -r '.DomainRecords.Record[0].Value' <<<"$response")

    if [[ "$record_id" != "null" && -n "$record_id" ]]; then
        [[ "$current_ip" == "$ip" ]] && return
        params="Action=UpdateDomainRecord&RecordId=$record_id&RR=$subdomain&SignatureMethod=HMAC-SHA1&SignatureNonce=$RANDOM&SignatureVersion=1.0&Timestamp=${timestamp}&Type=$type&Value=$ip&Version=2015-01-09"
    else
        params="Action=AddDomainRecord&DomainName=$DOMAIN&RR=$subdomain&SignatureMethod=HMAC-SHA1&SignatureNonce=$RANDOM&SignatureVersion=1.0&Timestamp=${timestamp}&Type=$type&Value=$ip&Version=2015-01-09"
    fi

    signature=$(aliyun_sign "$ALIYUN_SECRET_KEY" "$params")
    response=$(curl -s "http://alidns.aliyuncs.com/?$params&Signature=$(echo -n "$signature" | curl -Gso /dev/null -w %{url_effective} --data-urlencode @- "" | cut -c3-)" | jq . &>/dev/null)
    handle_dns_response "aliyun" "$type" "$subdomain" "$ip" "$response" || return 1
}

format_log() {
    local provider=$1 status=$2 type=$3 subdomain=$4 ip=$5 extra=$6
    printf "[%-8s] %-6s 类型:%-4s 记录:%-40s → %-25s %s\n" \
        "$provider" "$status" "$type" "${subdomain}.${DOMAIN}" "$ip" "$extra"
    log "$status" "$provider | $type | $subdomain.$DOMAIN$ip $extra"
}

# 通用响应处理函数
handle_dns_response() {
    local provider=$1 type=$2 subdomain=$3 ip=$4 response=$5
    case "$provider" in
    dnspod)
        local code=$(jq -r '.status.code' <<<"$response")
        local message=$(jq -r '.status.message' <<<"$response")
        local record_id=$(jq -r '.record.id' <<<"$response")

        if [[ "$code" == "1" ]]; then
            if [[ -n "$record_id" && "$record_id" != "0" ]]; then
                format_log "DNSPod" "成功" "$type" "$subdomain" "$ip" "(ID:$record_id)"
            else
                format_log "DNSPod" "创建" "$type" "$subdomain" "$ip"
            fi
        elif [[ "$code" == "104" ]]; then
            format_log "DNSPod" "记录已存在" "$type" "$subdomain" "$ip" "原因:$message"
            return 1
        elif [[ "$code" == "110" ]]; then
            format_log "DNSPod" "域名没有备案" "$type" "$subdomain" "$ip" "原因:$message"
            return 1
        else
            format_log "DNSPod" "失败" "$type" "$subdomain" "$ip" "Code:$code 原因:$message"
            return 1
        fi
        ;;
    cloudflare)
        local success=$(jq -r '.success' <<<"$response")
        if [[ "$success" == "true" ]]; then
            local record_id=$(jq -r '.result.id' <<<"$response")
            format_log "Cloudflare" "成功" "$type" "$subdomain" "$ip" "(ID:${record_id:0:8}...)"
        else
            local errors=$(jq -r '.errors[].message' <<<"$response" | tr '\n' ' ')
            format_log "Cloudflare" "失败" "$type" "$subdomain" "$ip" "原因:$errors"
            return 1
        fi
        ;;
    aliyun)
        local code=$(jq -r '.Code' <<<"$response")
        if [[ "$code" == "OK" ]]; then
            local record_id=$(jq -r '.RecordId' <<<"$response")
            format_log "Aliyun" "成功" "$type" "$subdomain" "$ip" "(ID:$record_id)"
        else
            local message=$(jq -r '.Message' <<<"$response")
            format_log "Aliyun" "失败" "$type" "$subdomain" "$ip" "原因:$message"
            return 1
        fi
        ;;
    esac
}

# 主处理流程
main() {
    log "INFO" "开始DDNS更新..."
    check_dependencies
    mkdir -p "$DATA_DIR"
    # vm
    for vmid in $(qm list | awk '$3 == "running" {print $1}'); do
        log "INFO" "处理虚拟机: $vmid"
        read mac hostname <<< $(get_vm_info "$vmid")
        [[ -z "$mac" ]] && continue

        read ipv4 ipv6 <<< $(get_ip_address "$vmid" "$mac")
        local rand_id=$(generate_rand_id "$vmid")

        [[ -n "$ipv4" ]] && {
            if $ENABLE_IPV4;then
                if $ENABLE_QUERY_PUBLIC_IPV4;then
                    ipv4=$(curl -s ipv4.icanhazip.com --connect-timeout 2)
                fi
            local subdomain="ipv4.${hostname}${rand_id}"
            case "$DNS_PROVIDER" in
                dnspod)    update_dnspod_dns "$subdomain" "A" "$ipv4" ;;
                cloudflare) update_cloudflare_dns "$subdomain" "A" "$ipv4" ;;
                aliyun)    update_aliyun_dns "$subdomain" "A" "$ipv4" ;;
            esac
            fi
        }

        [[ -n "$ipv6" ]] && {
            if $ENABLE_IPV6;then
            local subdomain="ipv6.${hostname}${rand_id}"
            case "$DNS_PROVIDER" in
                dnspod)    update_dnspod_dns "$subdomain" "AAAA" "$ipv6" ;;
                cloudflare) update_cloudflare_dns "$subdomain" "AAAA" "$ipv6" ;;
                aliyun)    update_aliyun_dns "$subdomain" "AAAA" "$ipv6" ;;
            esac
        fi
        }
    done
    # lxc
    for vmid in $(pct list | awk '$2 == "running" {print $1}'); do
        log "INFO" "处理LXC容器: $vmid"
        read mac hostname <<< $(get_lxc_info "$vmid")
        [[ -z "$mac" ]] && continue

        read ipv4 ipv6 <<< $(get_lxc_ip_address "$vmid" "$mac")
        local rand_id=$(generate_rand_id "$vmid")

        [[ -n "$ipv4" ]] && {
            if $ENABLE_IPV4;then
            local subdomain="ipv4.${hostname}${rand_id}"
            case "$DNS_PROVIDER" in
                dnspod)    update_dnspod_dns "$subdomain" "A" "$ipv4" ;;
                cloudflare) update_cloudflare_dns "$subdomain" "A" "$ipv4" ;;
                aliyun)    update_aliyun_dns "$subdomain" "A" "$ipv4" ;;
            esac
            fi
        }

        [[ -n "$ipv6" ]] && {
            if $ENABLE_IPV6;then
            local subdomain="ipv6.${hostname}${rand_id}"
            case "$DNS_PROVIDER" in
                dnspod)    update_dnspod_dns "$subdomain" "AAAA" "$ipv6" ;;
                cloudflare) update_cloudflare_dns "$subdomain" "AAAA" "$ipv6" ;;
                aliyun)    update_aliyun_dns "$subdomain" "AAAA" "$ipv6" ;;
            esac
        fi
        }
    done
    log "INFO" "DDNS更新完成"
}

# 执行入口
main
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2025-5-18 20:15

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表