在現今的網路環境中,無論是架設個人網站、NAS、遠端存取辦公室資源,擁有一個固定的網址來對應我們隨時可能變動的 IP 位址,都顯得至關重要。這篇文章將是您的終極指南,帶您從零開始,利用 Google Cloud Platform (GCP) 的永久免費資源與 Cloudflare 強大的 API,為您的 Draytek 路由器打造一個完全自主、零成本、支援多設備且極度穩定的動態 DNS (DDNS) 伺服器。
您可以將網域名稱 (例如 my-office.com) 想像成一個聯絡人的名字,而 IP 位址 (例如 114.33.22.11) 則是他的手機號碼。傳統的 DNS 就像一本靜態的電話簿,您手動將名字對應到號碼。但問題是,大多數家庭或小型辦公室的網路,其 IP 位址是由電信商動態分配的,就像手機號碼會變一樣。
DDNS (Dynamic DNS) 就是一位聰明的、全天候的通訊錄管家。當它發現您的 IP 位址變更時,會自動更新您的「電話簿」,確保別人透過您的名字 (my-office.com),永遠都能找到您最新的號碼 (最新的 IP 位址)。
結合這兩者,我們就能以零成本打造出比許多付費服務更具彈性、更安全且完全在自己掌控之下的 DDNS 系統。
由於我們的 GCP 伺服器本身也可能因為重啟而獲得新的動態 IP,所以我們將建立一個雙層的動態更新系統,確保服務永不中斷。
在開始之前,請確保您已擁有:
我們需要建立一個專用的 API 權杖,授權我們的程式更新 DNS 紀錄。
我們將使用Google Cloud的「Always Free」永久免費方案來架設伺服器。
現在我們要連線到VM,並放上核心的Python更新腳本。
2.安裝必要軟體:
在SSH終端機中,依序輸入以下指令來更新系統並安裝Python及pip:
sudo apt-get update
sudo apt-get install python3-venv python3-pip -y
3.為我們的專案建立虛擬環境:
python3 -m venv ddns-env
source ddns-env/bin/activate
pip install requests
4.建立GCP更新IP腳本檔案:
nano gcp_self_updater.py
import requests
import sys
# --- (1) 請修改您的設定 ---
CF_API_TOKEN = "貼上您從Cloudflare複製的API權杖"
ZONE_NAME = "domain.com" # <<--- 換成您自己的根網域名稱
SERVER_HOSTNAME = "ddns-server.domain.com" # <<--- 換成您要給伺服器用的網址
# --- 設定結束 ---
API_BASE_URL = "https://api.cloudflare.com/client/v4"
HEADERS = {
"Authorization": f"Bearer {CF_API_TOKEN}",
"Content-Type": "application/json"
}
def get_public_ip():
try:
response = requests.get('https://api.ipify.org?format=json', timeout=10)
response.raise_for_status()
return response.json()['ip']
except requests.RequestException as e:
raise Exception(f"無法獲取公網 IP: {e}")
def update_dns():
print("--- 開始執行伺服器自我 IP 更新 (v5.1) ---")
try:
new_ip = get_public_ip()
print(f"偵測到目前公網 IP: {new_ip}")
url = f"{API_BASE_URL}/zones"
response = requests.get(url, headers=HEADERS, params={'name': ZONE_NAME})
response.raise_for_status()
zone_id = response.json()['result'][0]['id']
url = f"{API_BASE_URL}/zones/{zone_id}/dns_records"
response = requests.get(url, headers=HEADERS, params={'name': SERVER_HOSTNAME, 'type': 'A'})
response.raise_for_status()
dns_records = response.json()['result']
current_ip = dns_records[0]['content'] if dns_records else None
if new_ip == current_ip:
print(f"IP 未變更 ({new_ip}),無需更新。")
return
print(f"伺服器 IP 已從 {current_ip} 變為 {new_ip},準備更新...")
# [官方文件優化] 明確加入 proxied: false
dns_record_data = {'type': 'A', 'name': SERVER_HOSTNAME, 'content': new_ip, 'ttl': 120, 'proxied': False}
if dns_records:
record_id = dns_records[0]['id']
url = f"{API_BASE_URL}/zones/{zone_id}/dns_records/{record_id}"
response = requests.put(url, headers=HEADERS, json=dns_record_data)
else:
url = f"{API_BASE_URL}/zones/{zone_id}/dns_records"
response = requests.post(url, headers=HEADERS, json=dns_record_data)
response.raise_for_status()
if response.json()['success']:
print(f"***** 伺服器 DNS 紀錄 '{SERVER_HOSTNAME}' 更新成功! *****")
else:
raise Exception(f"Cloudflare API 操作失敗: {response.json()['errors']}")
except Exception as e:
print(f"!!! 發生錯誤: {e} !!!", file=sys.stderr)
if __name__ == '__main__':
update_dns()
print("--- 執行完畢 ---")
5.修改設定:修改檔案中的 CF_API_TOKEN, ZONE_NAME, SERVER_HOSTNAME。
4.設定排程任務:
0 0 * * * /home/使用者名稱/venv/bin/python3 /home/使用者名稱/gcp_self_updater_requests.py >> /home/clone/gcp_self_updater.log 2>&1
這個腳本是核心,它會作為一個網站伺服器,接收來自 Draytek 路由器的請求。
1.建立腳本檔案:
nano ddns_updater.py
import http.server
import socketserver
from urllib.parse import urlparse, parse_qs
import sys
import requests
# --- (1) 請修改您的設定 ---
CF_API_TOKEN = "貼上您從Cloudflare複製的API權杖"
ZONE_NAME = "domain.com" # <<--- 換成您自己的根網域名稱
ROUTER_SECRETS = {
'office.domain.com': 'SuperSecretPasswordForOffice',
'home.domain.com': 'AnotherPasswordForHome',
}
# --- 設定結束 ---
API_BASE_URL = "https://api.cloudflare.com/client/v4"
HEADERS = {
"Authorization": f"Bearer {CF_API_TOKEN}",
"Content-Type": "application/json"
}
class DDNSHandler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
parsed_path = urlparse(self.path)
params = parse_qs(parsed_path.query)
hostname = params.get('hostname', [''])[0]
new_ip = params.get('ip', [''])[0]
secret = params.get('secret', [''])[0]
if ROUTER_SECRETS.get(hostname) != secret:
self.send_error_and_log(401, f"[{hostname}] 認證失敗")
return
print(f"[{hostname}] 認證成功,收到 IP: {new_ip}")
try:
url = f"{API_BASE_URL}/zones"
response = requests.get(url, headers=HEADERS, params={'name': ZONE_NAME})
response.raise_for_status()
zone_id = response.json()['result'][0]['id']
url = f"{API_BASE_URL}/zones/{zone_id}/dns_records"
response = requests.get(url, headers=HEADERS, params={'name': hostname, 'type': 'A'})
response.raise_for_status()
dns_records = response.json()['result']
current_ip = dns_records[0]['content'] if dns_records else None
if new_ip == current_ip:
print(f"[{hostname}] IP 未變更 ({new_ip}),無需更新。")
self.send_success_response("good", new_ip)
return
print(f"[{hostname}] IP 已從 {current_ip} 變為 {new_ip},準備更新...")
# [官方文件優化] 明確加入 proxied: false
dns_record_data = {'type': 'A', 'name': hostname, 'content': new_ip, 'ttl': 120, 'proxied': False}
if dns_records:
record_id = dns_records[0]['id']
url = f"{API_BASE_URL}/zones/{zone_id}/dns_records/{record_id}"
response = requests.put(url, headers=HEADERS, json=dns_record_data)
else:
url = f"{API_BASE_URL}/zones/{zone_id}/dns_records"
response = requests.post(url, headers=HEADERS, json=dns_record_data)
response.raise_for_status()
if not response.json()['success']:
raise Exception(f"Cloudflare API 操作失敗: {response.json()['errors']}")
print(f"***** [{hostname}] 操作成功!DDNS 更新完成! *****")
self.send_success_response("good", new_ip)
except Exception as e:
self.send_error_and_log(500, f"!!! [{hostname}] 發生嚴重錯誤: {e} !!!")
def send_success_response(self, status, ip):
self.send_response(200)
self.end_headers()
self.wfile.write(bytes(f"{status} {ip}", "utf-8"))
def send_error_and_log(self, code, message):
print(message, file=sys.stderr)
self.send_response(code)
self.send_header('Content-type', 'text/plain; charset=utf-8')
self.end_headers()
self.wfile.write(message.encode('utf-8'))
def log_message(self, format, *args):
return
if __name__ == "__main__":
with socketserver.TCPServer(("", PORT), DDNSHandler) as httpd:
print(f"多設備 DDNS 伺服器 (v5.1) 已在通訊埠 {PORT} 啟動...")
httpd.serve_forever()
2.修改設定:修改檔案中的 CF_API_TOKEN, ZONE_NAME, ROUTER_SECRETS。
3.設定為系統服務:
sudo nano /etc/systemd/system/ddns_updater.service
[Unit]
Description=Cloudflare DDNS Updater Service
After=network.target
[Service]
User=使用者名稱
Group=使用者名稱
WorkingDirectory=/home/使用者名稱/
Environment="PYTHONUNBUFFERED=1"
ExecStart=/home/使用者名稱/ddns-env/bin/python3 /home/使用者名稱/ddns_updater.py
Restart=always
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl start ddns_updater.service
sudo systemctl enable ddns_updater.service
這是將一切串連起來的最後一步。
恭喜您!至此,您已經完成了所有伺服器的部署與設定。現在,讓我們進行最後的驗證,確保整個系統如預期般完美運作。
1.即時監控服務日誌
首先,在您的 GCP VM SSH 終端機視窗中,執行以下指令來即時監看 DDNS 伺服器的活動日誌:
journalctl -u ddns_updater.service -f
2.從 Draytek 強制觸發更新
接著,登入您的 Draytek 路由器管理介面,前往 應用 (Applications) >> 動態DNS (Dynamic DNS)。在您設定好的 User-Defined 設定檔那一列,點擊右側的 Force Update 按鈕。
3.觀察日誌輸出
點擊 Force Update 後,立刻回到您第一步的 GCP VM SSH 視窗。如果一切設定正確,您應該會即時看到類似以下的成功訊息:
Jul 20 21:38:00 ddns-server python3[...]: [office.domain.com] 認證成功,收到 IP: 18.163.XX.XX
Jul 20 21:38:04 ddns-server python3[...]: [office.domain.com] IP 已從 1.25.3.204 變為 18.163.XX.XX,準備更新...
Jul 20 21:38:05 ddns-server python3[...]: ***** [office.domain.com] 操作成功!DDNS 更新完成! *****
看到這些訊息,就代表您的路由器與伺服器之間的通訊與認證已經暢通無阻。
4.在 Cloudflare 確認最終結果
作為最終的驗證,您可以登入您的 Cloudflare 儀表板,進入對應的網域名稱,檢查 DNS 紀錄。您會發現該筆 A 紀錄 (例如 office.domain.com) 的 IP 位址,已經成功更新為您路由器當前的最新 IP。
依照本篇教學的完整步驟,您已成功建立一個完全自主、零成本、極度穩定且支援多設備的企業級 DDNS 系統。您不僅擺脫了對第三方付費服務的依賴,更重要的是,將網路的核心控制權牢牢掌握在自己手中。
這套方法的優美之處在於其核心邏輯的通用性。今天我們以 Draytek 為範例,但同樣的伺服器架構完全可以延伸應用到任何支援自訂 DDNS URL 的設備上,例如 Ubiquiti UniFi、Synology/QNAP NAS,甚至是您家中的任何一台個人電腦或 Linux 主機,只需讓它們定時呼叫您伺服器上的那個特定網址即可。
我們未來也計畫推出針對不同品牌設備的設定教學,協助您將這套強大的系統應用到更多的場景中,敬請期待!