步骤
- 光猫改桥接。
- 路由器使能ipv6。
- 域名托管到cloudflare,可使用免费注册的dpdns.org域名。
- 获取cloudflare API token。
- cloudflare中增加一条AAAA记录,Name填: ddnsv6,Content填写: 240e:341:: ,
- 在内网ubuntu主机上配置DDNS脚本: cloudflare_ddns_ipv6.sh,修改其中的CF_API_TOKEN、CF_ZONE_ID、RECORD_NAME。
- CF_API_TOKEN为第4步中申请的token。
- CF_ZONE_ID在cloudflare的主机中,点击xxx.dpdns.org域名,进入页面的右下角查看。
- RECORD_NAME为第5步中添加的AAAA记录,比如ddnsv6.xxx..dpdns.org。
- crontab -l定时执行脚本
*/15 * * * * /usr/local/bin/cloudflare_ddns_ipv6.sh
- cloudflare_ddns_ipv6.sh内容如下:
#!/usr/bin/env bash # === Configuration === # --- Cloudflare API --- # Create a Cloudflare API Token with Zone:DNS:Edit permissions for your specific zone CF_API_TOKEN="YOUR_CLOUDFLARE_API_TOKEN" # Replace with your token CF_ZONE_ID="YOUR_CLOUDFLARE_ZONE_ID" # Replace with your Zone ID # --- DNS Record --- RECORD_NAME="your_subdomain.yourdomain.com" # Replace with the FQDN to update (e.g., home.example.com) RECORD_TYPE="AAAA" # Use "A" for IPv4, "AAAA" for IPv6 # --- Behaviour --- CF_PROXIED="false" # Use "true" for proxied (Orange Cloud), "false" for DNS only (Grey Cloud) # --- State & Logging --- # Optional: Use absolute paths if running from cron STATE_FILE="/tmp/cf_ddns_ipv6.lastip" # Stores the last known public IP address LOG_FILE="/tmp/cloudflare_ddns_ipv6.log" # Log file location # --- External IP Service --- # Service that returns *only* your public IPv6 address IPV6_SERVICE="https://api64.ipify.org" # Alternatives: ip6.icanhazip.com, ifconfig.me/ip (sometimes returns v4/v6) # === End Configuration === # === Helper Functions === log() { echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE" # Optionally echo to stdout as well if running interactively # echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" } # Basic IPv6 Address Regex (adjust if needed for specific formats like ::1) # This regex is simplified and checks for common structures. is_ipv6() { [[ "$1" =~ ^([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])$ ]] } # === Sanity Checks === # Check if required tools exist if ! command -v curl &> /dev/null; then echo "Error: curl is not installed." >&2 exit 1 fi if ! command -v jq &> /dev/null; then echo "Error: jq is not installed." >&2 exit 1 fi # Check if configuration seems valid (basic checks) if [[ "$CF_API_TOKEN" == "YOUR_CLOUDFLARE_API_TOKEN" ]] || [[ -z "$CF_API_TOKEN" ]]; then echo "Error: CF_API_TOKEN is not configured." >&2; exit 1; fi if [[ "$CF_ZONE_ID" == "YOUR_CLOUDFLARE_ZONE_ID" ]] || [[ -z "$CF_ZONE_ID" ]]; then echo "Error: CF_ZONE_ID is not configured." >&2; exit 1; fi if [[ "$RECORD_NAME" == "your_subdomain.yourdomain.com" ]] || [[ -z "$RECORD_NAME" ]]; then echo "Error: RECORD_NAME is not configured." >&2; exit 1; fi if [[ "$RECORD_TYPE" != "AAAA" ]]; then echo "Error: This script is configured for AAAA records, but RECORD_TYPE is set to '$RECORD_TYPE'." >&2; exit 1; fi # Create log file directory if it doesn't exist LOG_DIR=$(dirname "$LOG_FILE") if [[ ! -d "$LOG_DIR" ]]; then mkdir -p "$LOG_DIR" || { echo "Error: Could not create log directory '$LOG_DIR'." >&2; exit 1; } fi # Ensure log file is writable touch "$LOG_FILE" || { echo "Error: Could not write to log file '$LOG_FILE'." >&2; exit 1; } # === Main Logic === log "Starting DDNS update check for ${RECORD_NAME}..." # --- Get Current Public IPv6 Address --- CURRENT_IPV6=$(curl -s -6 "$IPV6_SERVICE") # Use -6 to force IPv6 request if [[ $? -ne 0 ]]; then log "Error: Failed to contact IPv6 service '$IPV6_SERVICE'." exit 1 fi # Validate the fetched IP if ! is_ipv6 "$CURRENT_IPV6"; then # Sometimes services return error messages or IPv4 if IPv6 isn't available log "Error: Fetched IP '$CURRENT_IPV6' from '$IPV6_SERVICE' does not look like a valid IPv6 address." exit 1 fi log "Current public IPv6: ${CURRENT_IPV6}" # --- Get Last Known IP Address --- LAST_IPV6="" if [[ -f "$STATE_FILE" ]]; then LAST_IPV6=$(cat "$STATE_FILE") log "Last known IP from state file: ${LAST_IPV6}" else log "State file '${STATE_FILE}' not found. Will force update." fi # --- Compare IP Addresses --- if [[ "$CURRENT_IPV6" == "$LAST_IPV6" ]]; then log "IP address unchanged (${CURRENT_IPV6}). No update needed." exit 0 fi log "IP address changed (Current: ${CURRENT_IPV6}, Previous: ${LAST_IPV6}). Updating Cloudflare..." # --- Cloudflare API Interaction --- # Common Headers HEADERS=(-H "Authorization: Bearer ${CF_API_TOKEN}" -H "Content-Type: application/json") # --- Get Record ID --- API_URL_GET="https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records?type=${RECORD_TYPE}&name=${RECORD_NAME}" log "Fetching Record ID from Cloudflare for ${RECORD_NAME}..." RECORD_INFO_JSON=$(curl -s -X GET "$API_URL_GET" "${HEADERS[@]}") if [[ $? -ne 0 ]]; then log "Error: Failed to connect to Cloudflare API (GET)." exit 1 fi # Check if the API call was successful before parsing SUCCESS=$(echo "$RECORD_INFO_JSON" | jq -r '.success') if [[ "$SUCCESS" != "true" ]]; then ERRORS=$(echo "$RECORD_INFO_JSON" | jq -r '.errors | map("\(.code) \(.message)") | join(", ")') log "Error: Cloudflare API (GET) failed. Errors: ${ERRORS:-Unknown API error, check JSON response}" log "API Response (GET): ${RECORD_INFO_JSON}" exit 1 fi # Extract Record ID RECORD_ID=$(echo "$RECORD_INFO_JSON" | jq -r '.result[0].id') # Get ID of the first matching record if [[ -z "$RECORD_ID" ]] || [[ "$RECORD_ID" == "null" ]]; then log "Error: Could not find Record ID for ${RECORD_NAME} (Type: ${RECORD_TYPE}) in zone ${CF_ZONE_ID}." log "API Response (GET): ${RECORD_INFO_JSON}" log "Check if the record exists in your Cloudflare DNS settings." # Optional: Create the record if it doesn't exist (more complex, involves POST request) # log "Attempting to create the record..." exit 1 fi log "Found Record ID: ${RECORD_ID}" # --- Update DNS Record --- API_URL_PUT="https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records/${RECORD_ID}" log "Updating DNS record ${RECORD_ID} for ${RECORD_NAME} to ${CURRENT_IPV6}..." # Construct JSON payload using jq for safety with special characters JSON_PAYLOAD=$(jq -n \ --arg type "$RECORD_TYPE" \ --arg name "$RECORD_NAME" \ --arg content "$CURRENT_IPV6" \ --argjson proxied "$CF_PROXIED" \ '{"type": $type, "name": $name, "content": $content, "ttl": 1, "proxied": $proxied}') # ttl:1 = Auto UPDATE_RESPONSE_JSON=$(curl -s -X PUT "$API_URL_PUT" "${HEADERS[@]}" --data "$JSON_PAYLOAD") if [[ $? -ne 0 ]]; then log "Error: Failed to connect to Cloudflare API (PUT)." exit 1 fi # Check if the update was successful SUCCESS=$(echo "$UPDATE_RESPONSE_JSON" | jq -r '.success') if [[ "$SUCCESS" == "true" ]]; then log "Success: Cloudflare DNS record updated successfully for ${RECORD_NAME} to ${CURRENT_IPV6}." # --- Update State File --- echo "$CURRENT_IPV6" > "$STATE_FILE" if [[ $? -ne 0 ]]; then log "Warning: Failed to update state file '${STATE_FILE}'." else log "State file '${STATE_FILE}' updated." fi exit 0 else ERRORS=$(echo "$UPDATE_RESPONSE_JSON" | jq -r '.errors | map("\(.code) \(.message)") | join(", ")') log "Error: Cloudflare API (PUT) failed. Errors: ${ERRORS:-Unknown API error, check JSON response}" log "API Response (PUT): ${UPDATE_RESPONSE_JSON}" exit 1 fi
- 通过如命令查看日志,每15分钟会配置一次。
cat /tmp/cloudflare_ddns_ipv6.log
验证网站
test-ipv6.com
其它可尝试方案
- ddns-go取代cloudflare_ddns_ipv6.sh。