跳轉到

2.3 DDNS 與內網穿透:多線路動態 IP 與 NAT 環境下的解法#

本節目標#

  • 認知家寬/自架環境的根本困境:浮動 IP 與 NAT 無法被外部直達
  • 實作第一層解法:用 Cloudflare API 撰寫 DDNS client 並以 cron 定時更新
  • 理解第二層限制:IPv4 連接埠分享(NAT/port forwarding)在多線路、CGNAT 環境的失效
  • 預告正確解法:Cloudflare Tunnel(第 8 章實作)為何能把問題從「开入站」變成「拉出站」

問題一:你的 IP 每天在變#

一般家用寬頻的「固定 IP」是加購服務;預設情況 ISP 透過 DHCP 定期重新發權證,你也可能遇到:

情境 後果
重啟數據機 / 停電復電 換 IP
ISP 實施更短租約(有些 24h) 每天換 IP
多線路負載平衡(2 條寬頻自動切換) 出口 IP 隨路由漂移
轉成 IPv6-only 或 CGNAT 外部根本無法指定你

DNS 的答案本質是「這個名稱今天在這個 IP」。名稱不變、IP 一直在變 → 這就是 DDNS(Dynamic DNS) 要解的問題:自動在 IP 變化時更新 DNS 記錄。

歷史上 DDNS 由 DynDNS(2000s 早期開始)帶起,實作精神都一樣:週期性測出自己的公網 IP → 呼叫 DNS 服務商 API 更新記錄。Cloudflare 免費帳號即可做到(免費方案還有 API token 精細權限,比「一組帳密全開」安全得多)。

實作:cloudflare-ddns client + cron#

步驟 1:建立最小權限 API token#

Dashboard → My Profile → API Tokens → Create Token,選「Edit zone DNS」範本:

設定 值
Permissions Zone / DNS / Edit
Zone Resources 只選你的網域
Client IP filtering 可填你住宅的出口 IP(多線路時跳過)

千萬不要用 Global API Key——它的權限是整個帳號全開,工寬環境洩漏代價極大。

步驟 2:取得 zone id 與記錄資訊#

curl -s -H "Authorization: Bearer $TOKEN" \
  https://api.cloudflare.com/client/v4/zones?name=example.com | jq .result[0].id

步驟 3:更新腳本(Shell 版)#

#!/bin/sh
# ddns-cloudflare.sh — 用一次 API 呼叫 idempotent 更新 A 記錄
TOKEN="你的token"        # 實務上放 600 檔案權限的 env 檔
ZONE_ID="你的zone id"
RECORD_ID="你的record id"
NAME="home.example.com"

IP=$(curl -s https://www.cloudflare.com/cdn-cgi/trace | awk '/^ip=/{print $2}')
[ -z "$IP" ] && echo "無法偵測公網 IP" && exit 1
echo "$(date) 偵測 IP: $IP"

curl -s -X PATCH \
  "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$RECORD_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  --data "{\"type\":\"A\",\"name\":\"$NAME\",\"content\":\"$IP\",\"ttl\":60,\"proxied\":false}"

三個設計重點:

  1. 直接抓 Cloudflare 自己告訴你的出口 IP(/cdn-cgi/trace)比自己寫「我的 IP 是什麼」服務更可靠——Cloudflare 就是 DNS 記錄的目的地,兩邊看到的視角一致。
  2. TTL 設 60(允許的最小值):DDNS 的 IP 會變,快取租約必須短。這是 2.1 節 TTL 語意的直接應用。
  3. 腳本是 idempotent(重複執行=同一結果):cron 每 5 分鐘打一次也沒有副作用。

步驟 4:掛進 cron(或 systemd timer)#

crontab -e
# 每 5 分鐘更新一次,log 寫到關閉 rock
*/5 * * * * /home/user/ddns-cloudflare.sh >> /var/log/ddns.log 2>&1

現成替代品:開源工具 [cloudflare-ddns](Go/Python 版)或路由器內建 DDNS 功能(多半寫死在 DDNS 服務商,對 Cloudflare 支援不一)。

問題二:就算名稱解析對了,流量也進不來#

DDNS 只解了「找到你」,還沒解「連到你」。家庭寬頻幾乎都在 NAT 後:路由器代表全家人共用一個公網 IPv4,外部想連入 80/443 需要連接埠轉發(port forwarding),把「WAN:443 → 內網 192.168.1.10:443」寫進路由器。

多線路環境讓這更麻煩:

                ┌── line A (ISP-A, IP-A) ──┐
 內網 LAN ──路由器│  loadbalance policy      │── 網際網路
                └── line B (ISP-B, IP-B) ──┘

外部連 IP-A 或 IP-B 都要分別設定 NAT 規則;回程封包要從進來的那條線路出去(asymmetric routing),家用路由器幾乎設定不來。更糟的是 CGNAT(Carrier-Grade NAT):ISP 把幾百戶共用一個公網 IPv4,你根本拿不到屬於自己的公網位址,連接埠轉發無從寫起。

再加一層現實:家用寬頻的 80/443 常被 ISP 過濾(防殭屍網路),你把 NAT 規則配好了,外面還是連不進來。

傳統入站模式的層層關卡:

 外部使用者 → DNS(IP 會飄) → ISP 過濾 80/443? → NAT 連接埠轉發
 → 多線路路由正確性? → 內網伺服器

每關都可能失效,且失效時你無法控制(ISP、路由器、租約皆非你所有)。

下一層解法:反轉方向,讓連線「拉出站」#

傳統模式的根本困難在於:伺服器是被動的,得等外部進來。Tunnel 的做法是讓內網機器主動 maintain 一條出站 TCP 連線到 Cloudflare 邊緣——出站連線在 NAT 與防火牆的世界幾乎永遠通行(你的瀏覽器每天都在做),於是:

  • 內網不需要任何連接埠轉發
  • 公網 IP 變不變無關緊要(連線是機器主動建立的長連線)
  • 80/443 被 ISP 封鎖也無妨(Tunnel 從內到外發起新連線,出站無阻)
  • Cloudflare 變成「外部入口 + 隱藏源站 IP」的代理層
九十年代模型:  使用者 ──直接進站──> 你的家寬(被動)   ←一連串關卡
Tunnel 模型:  家寬機器 ──主動出站──> Cloudflare 邊緣 <──使用者進站

第五章起將用小 daemon(cloudflared)首次實作 Tunnel。本節先建立直覺:DDNS 解「名稱跟得上 IP」,Tunnel 解「入站根本是被迫的」——兩者常被混為一談,但結構上 Tunnel 徹底跳出了那層限制。

小結與練習#

浮動 IP 用 DDNS(cron + Cloudflare API、最小權限 token、短 TTL);動態 NAT/多線路/CGNAT 用連接埠轉發的的失敗,引出「出站長連線 + 邊緣代理」才是可攜式解法。

練習:

  1. 寫出你的 ISP 環境事件樹:停電、換線路、CGNAT 各會讓傳統入站模型在哪一層斷掉?
  2. 為什麼 DDNS 腳本要設 ttl: 60?若忘記把舊 TTL 降到 60 會發生什麼?
  3. 改寫腳本使用 curl -s --fail 並在失敗時重試 3 次(作業:補上重試迴圈)。
  4. 比較「DDNS + port forwarding」與「Tunnel」在安全面差異:哪一種把「源站 IP」暴露在 DNS 記錄裡?