自架 DDNS 教學:用 Cloudflare API 達成多域名同步更新!

August 28, 2023

cover
Source: lab.magiconch.com

2017 年,我把我的家庭伺服器在 NUC 上架了起來,其中一個很重要的目的是為了架自己的 VPN,所以首要任務就是要搞 DDNS,為了要能夠在各地連回來。

當時,經過一番研究後,發現 Cloudflare 有提供 DNS 的 API,而我剛好把我的域名都架在 Cloudflare 上。我上網拼拼湊湊出了一段 ddns.py 程式

import secret
import requests

ip_r = requests.get('http://icanhazip.com/')
ip = ip_r.text.strip()

headers = {'X-Auth-Email': secret.CF['email'],
           'X-Auth-Key':   secret.CF['key'],
           'Content-Type': 'application/json'}

params = {'type':    'A',
          'name':    'den.rexy.xyz',
          'content': ip}

slugs = {'endpoint': 'https://api.cloudflare.com/client/v4',
         'zone_id':  secret.HOME['zone_id'],
         'id':       secret.HOME['id']}
url = '{endpoint}/zones/{zone_id}/dns_records/{id}'.format(**slugs)

r = requests.put(url, headers=headers, json=params)

if r.json()['success'] == True:
    print('updated')

這個程式的邏輯很簡單,就是先從 icanhazip 拿自己的公開 IP,然後發一個更新 DNS 紀錄的 API 給 Cloudflare,讓他重新把我的 den.rexy.xyz 指向我的公開 IP;其中 API 需要用的 email、key、各種 id 都存在另外一個不會放進 git 版本控制的 secret.py 裡。然後我用 cron 設定每幾個小時讓他跑一次,確保資料更新。就這樣,我就一直用這個小程式用到畢業。

直到最近,我開始又對我的小電腦 NUC 產生興趣,想要拿他來做點事情,於是就決定把一切砍掉重來,當然也包括這個 ddns.py 程式。

思考重寫的時候,我想說現在我已經不再是那個對於 shell script 恐懼的我了,就直接寫一段 shell script 好了。於是我就又開始重新研究了一番 Cloudflare API,這次終於比較懂了。先直接貼上更新後的 ddns.sh 版本

#!/bin/bash

SECRET=$(dirname $(realpath "$0"))/secret.sh
[ -e "$SECRET" ] || (echo "secret.sh does not exist."; exit 1)
. $SECRET

CURRENT_IP=$(curl -s http://ipv4.icanhazip.com)
DNS_IP=$(dig +short "${URLS[0]}" | grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' | awk 'NR==1{print}')

if [ "$CURRENT_IP" != "$DNS_IP" ]; then
    echo "Renew IP: $DNS_IP to $CURRENT_IP"
    for ((i = 0; i < ${#URLS[@]}; i++)); do
        curl --request PUT \
            --url "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/${RECORD_IDS[$i]}" \
            --header "Content-Type: application/json" \
            --header "X-Auth-Email: $EMAIL" \
            --header "Authorization: Bearer $KEY" \
            --data "{
        \"type\": \"A\",
        \"name\": \"${URLS[$i]}\",
        \"content\": \"$CURRENT_IP\"
        }"
    done
else
    echo "No change: $CURRENT_IP"
fi

這個更新後的版本有很多地方是叫 ChatGPT 寫的,所以我也不清楚是不是萬無一失。首先第一個不一樣的地方是,我這次寫的版本要更新多個 DNS 紀錄,所以我的 secret.sh 長得是像這樣子:

EMAIL='[email protected]'
KEY='key'
ZONE_ID='zone-id'
RECORD_IDS=('id-for-one'
            'id-for-two')
URLS=('one.example.com'
      'two.example.com')

第一步跟之前不一樣的地方是他做了一個優化,我們會先去問 icanhazip 拿我們現在實際的公開 IP,再去查在 Cloudflare DNS 上我們域名登記的 IP,然後比較兩個 IP。如果兩個 IP 一樣,代表不需要更新,如果不一樣,我們才繼續做更新,這樣就不會一直無謂的去做 API call 了。

接下來就是要跟 Cloudflare API 溝通的部分了,我們先去申請好我們的需要的 key 鑰匙。移動到 dash.cloudflare.com/profile > 左邊選 API Tokens > 點選 Create Token > 點選 Edit zone DNS > Use template > 在 Zone Resources 那欄最右邊選擇你要用的域名 > Continue to summary > Create Token。然後他就會顯示給你看你這把鑰匙了,這個一定要複製下來收好喔,因為他之後就不會再顯示了。你可以複製下面那段 curl 命令,測試一下這個鑰匙的確是能用的。

1

而要更新 DNS 紀錄,我們可以複習一下 Cloudflare 的 API 文檔

PUT https://api.cloudflare.com/client/v4/zones/{zone_identifier}/dns_records/{identifier}

Update an existing DNS record.

而這個 zone_identifier 要去哪裡找呢?他在 dash.cloudflare.com > Cloudflare Home > example.com Overview 的右下角:

2

而後面這個 identifier 是指屬於你這個 DNS 紀錄的 record ID,我們需要特別去連 API 找一下他:

curl --request GET \
  --url https://api.cloudflare.com/client/v4/zones/{zone_identifier}/dns_records?name={domain_name} \
  --header 'Content-Type: application/json' \
  --header 'X-Auth-Email: {email}' \
  --header 'Authorization: Bearer {key}'

這裡的 zone_identifier 跟上面的是一樣的。domain_name 是你這個 DNS 記錄的總名稱,例如 www.example.com。email 就是 Cloudflare 帳號。最後這個 key 就是剛剛申請的鑰匙。而這個發過去後,會得到類似這樣的回覆:

{
    "result": [
        {
            "id": "{identifier}",
            "zone_id": "...",
            "zone_name": "...",
            ...
        }
    ],
    "success": true,
    ...
}

這 result 裡的 id 後面那串就是那個 {identifier} 了!

把這些資訊代入一開始準備好的 secret.sh,就大功告成了!接下來就設定讓他每隔一段時間就跑一次,就能達成穩定同步 DDNS 的效果了!例如我的設置就是用 cron 每個小時跑一次。


如果你一更新完就去用 dig 檢查 DNS 的話可能會發現沒有改變,這是因為快取還沒更新的關係。我們可以指定要用 Cloudflare 的 name server 就可以得到不經過快取的更新後的正確結果:

dig @bruce.ns.cloudflare.com {domain_name}

更新:如果是在沒有 dig 的環境上要使用,例如 BusyBox,可以用以下的 nslookup 取代(ChatGPT 寫的)

DNS_IP=$(nslookup "$URL" | grep -Eo 'Address: ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)' | cut -d' ' -f2)