PVE下借助QGA对虚拟机进行DDNS
pve 平台对虚拟机进行 DDNS 的方案有很多,今天花了点时间自己写了一个。
适用于提取安装了 QGA(Qemu-Guest-Agent)服务的虚拟机
功能实现的核心是以下几行命令
qm config "$vmid"
qm guest cmd "$vmid" get-host-name
qm guest cmd "$vmid" network-get-interfaces
qm list
qm guest exec "$vmid" ip a
优化一下可以得到这些工具函数
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}"
}
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)
echo "$ipv4 $ipv6"
}
整合一下 dns 运营商通过的 api 就可以实现 DDNS 了
update_dnspod_dns() {
local subdomain=$1 type=$2 ip=$3
local api_url="https://dnsapi.cn/Record.Ddns"
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
ENABLE_IPV4=true
ENABLE_QUERY_PUBLIC_IPV4=true
ENABLE_IPV6=true
PUBLIC_IPV4_QUERY_LINK=https://myip.ipip.net
DOMAIN="yourdomain.com"
DATA_DIR="/root/ddns/data"
DNS_PROVIDER="dnspod"
DNSPOD_API_TOKEN="ID,token"
CLOUDFLARE_API_TOKEN=""
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"
}
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}"
}
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)
echo "$ipv4 $ipv6"
}
update_dnspod_dns() {
local subdomain=$1 type=$2 ip=$3
local api_url="https://dnsapi.cn/Record.Ddns"
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
}
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
}
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
效果
|