検索

記事と日記を検索…

Caddy + Cloudflare 自動更新動態 IPv6 DDNS

2026-10-08 に記す・寒露

この記事はまだ日本語に翻訳されていません。原文を表示しています。

他の言語: 繁體中文

最近把電信商從 300/50 換成中華電信的 300/300,換完之後家裡有了 IPv6,想來搞點事情。

但中華電信給的 IPv6 是動態的。重新撥號、Router 重開、Prefix Delegation 更新,或是 ISP 調整網路配置,位址都有可能換掉。

先假設 Caddy 目前的 IPv6 是這個(文中的位址都是範例,不是我家的真實位址):

テキスト
2001:db8:1234:5678:1a2b:3cff:fe4d:5e6f

Cloudflare 上的 coder.sao-x.com 有一筆 AAAA 指向它。問題是 ISP 一旦換了位址,Cloudflare 那邊沒跟著改,DNS 就會繼續指到舊的 IPv6,連線直接失敗。

解法是用 Caddy 的 caddy-dynamicdns 模組:讓 Caddy 定期檢查自己的 IPv6,發現變了就去改 Cloudflare 的 DNS。這個模組本來就是拿來維護 A/AAAA 紀錄,讓 DNS 一直指向主機目前的 IP。

我的環境

Proxmox 上的一台 LXC 跑 Caddy,同時負責 HTTPS、反向代理、Cloudflare DNS,現在連 DDNS 也一起做了。

IPv6 IPv6 /64 Dynamic DNS IPv4 reverse proxy Internet 家用 Router Proxmox vmbr0 Caddy LXC192.168.0.2 + 動態 IPv6 Cloudflare AAAA Coder VM192.168.0.38

先確認 Caddy 有編進 DDNS 模組

Bash
caddy list-modules --packages | grep -E 'caddy-dns/cloudflare|caddy-dynamicdns'

應該會看到這三行:

テキスト
dns.providers.cloudflare github.com/caddy-dns/cloudflare
dynamic_dns github.com/mholt/caddy-dynamicdns
dynamic_dns.ip_sources.interface github.com/mholt/caddy-dynamicdns

dns.providers.cloudflare 負責讓 Caddy 去操作 Cloudflare DNS,dynamic_dns 負責偵測 IP 有沒有變、有變就更新。caddy-dynamicdns 的 DNS provider 是模組化的,所以一定要搭配 caddy-dns 的 provider 才能用。

確認 LXC 拿到 IPv6

在 Caddy LXC 裡看位址:

Bash
ip -6 addr

會看到類似這樣的一行:

テキスト
inet6 2001:db8:1234:5678:1a2b:3cff:fe4d:5e6f/64 scope global dynamic

再看路由:

Bash
ip -6 route
テキスト
2001:db8:1234:5678::/64 dev eth0
default via fe80::.... dev eth0

然後測一下能不能連到外面:

Bash
ping -6 -c 4 2606:4700:4700::1111
curl -6 https://ifconfig.co

curl 如果回傳的是 2001:db8:1234:5678:.... 這種全球位址,就代表這台 LXC 的 IPv6 通到 Internet 了。

Cloudflare API Token

Cloudflare 的 Caddy 模組現在建議用單一 API Token,權限至少要有:

  • Zone.Zone:Read
  • Zone.DNS:Edit

範圍限制在你要管的 Zone,像我是 sao-x.com。

Caddyfile 裡不用把 Token 寫死,可以用環境變數:

テキスト
provider cloudflare {env.CLOUDFLARE_API_TOKEN}
テキスト
CLOUDFLARE_API_TOKEN=xxxxxxxxxxxxxxxx

Token 不要外流

真的 API Token 不要貼到公開文章、GitHub repo 或聊天紀錄裡。

Caddyfile 設定

在 /etc/caddy/Caddyfile 的 global options 加上 dynamic_dns:

テキスト
{
    dynamic_dns {
        provider cloudflare {env.CLOUDFLARE_API_TOKEN}

        domains {
            sao-x.com
        }

        versions ipv6
        ip_source interface eth0
    }

    email only@sao-x.com

    metrics {
        per_host
    }

    log {
        output stdout
        format json
        level INFO
    }

    order crowdsec first

    crowdsec {
        api_url http://127.0.0.1:8080
        api_key YOUR_CROWDSEC_API_KEY
        ticker_interval 15s
    }
}

(crowdsec_check) {
    log
    crowdsec
}

import /etc/caddy/sites/*

幾個關鍵行:

  • provider cloudflare {env.CLOUDFLARE_API_TOKEN}:用哪個 DNS provider、Token 從哪來。
  • versions ipv6:只管 IPv6,也就是只更新 AAAA。
  • ip_source interface eth0:從 eth0 這張網卡讀目前的 IPv6。

官方文件的 IPv6-only 範例,也是 versions ipv6 加 ip_source interface eth0 這個組合。

domains 怎麼填

這裡最容易寫錯。語法是:

テキスト
<zone> <record-name>...

<zone> 後面接的是相對於這個 Zone 的名稱,@ 代表 Zone 本身:

テキスト
domains {
    example.com @ www
}

管理的是 example.com 和 www.example.com。

不要在 domains 裡寫 wildcard

像下面這樣寫是不行的:

テキスト
domains {
    sao-x.com *.sao-x.com
}

domains 不是 wildcard pattern,不會幫你涵蓋所有子網域。要管哪些,就一個一個列出來。

用 dynamic_domains 自動抓 Caddy 裡的網站

如果 Caddy 裡已經配了一堆站台,像是:

テキスト
coder.sao-x.com
dashboard.sao-x.com
nas.sao-x.com
vw.sao-x.com
blog-api.sao-x.com

手動一個個列很麻煩,這時可以加 dynamic_domains:

テキスト
dynamic_dns {
    provider cloudflare {env.CLOUDFLARE_API_TOKEN}

    domains {
        sao-x.com
    }

    dynamic_domains

    versions ipv6
    ip_source interface eth0
}

它會去掃 Caddy HTTP app 裡已經配好的 hostname,再嘗試管理對應的 DNS 紀錄。官方文件特別提到,只有 top-level server routes 的 host matcher 會被掃到。

所以像這種寫法就會被抓到:

テキスト
coder.sao-x.com {
    reverse_proxy 192.168.0.38:8080
}

Caddy 裡有 wildcard site 的話

如果你的 Caddy 有這種站台:

テキスト
*.sao-x.com {
    ...
}

別以為 Dynamic DNS 會幫所有可能的 xxx.sao-x.com 都建一筆 AAAA。dynamic_domains 掃的是 Caddy 裡實際配置的 hostname,不會把 wildcard 展開成無限多筆紀錄。

如果 Cloudflare 裡的 *.sao-x.com 本身就是一筆 wildcard DNS record,那要另外想辦法處理那一筆。

驗證並 reload

改完先別急著 reload,先驗證:

Bash
caddy validate --config /etc/caddy/Caddyfile

正常會看到 Valid configuration。

如果看到的是:

テキスト
API token '' appears invalid

表示 Caddy 啟動時沒拿到 CLOUDFLARE_API_TOKEN。這是環境變數沒載入的問題,跟 domains 的寫法無關。Caddy 官方文件也有提醒:{env.*} 是 runtime 的環境變數 placeholder,用 systemd 跑 Caddy 的話,必須確定它真的拿得到這個變數。

驗證通過之後:

Bash
systemctl reload caddy
systemctl status caddy --no-pager
journalctl -u caddy -n 100 --no-pager

檢查 Cloudflare 的 AAAA

Bash
dig AAAA coder.sao-x.com @1.1.1.1

應該會回目前 Caddy 的 IPv6:

テキスト
coder.sao-x.com. 300 IN AAAA 2001:db8:1234:5678:1a2b:3cff:fe4d:5e6f

也可以跟 curl -6 https://ifconfig.co 的結果比一下,兩邊應該是同一個位址。

為什麼用 interface eth0

這台 Caddy LXC 的網卡就叫 eth0,所以 ip_source interface eth0 會直接從這張網卡找目前可用的 global IPv6。官方文件說明,interface 這個 IP source 能取得網卡上的 IPv4/IPv6,並且會排除 private 和非 global 的位址。

這樣就不用另外 curl ifconfig.co 去查,也不用裝 ddclient、inadyn 或其他 DDNS client,Caddy 自己編進去的模組就夠了。

Router 的 IPv6 防火牆要自己開

DDNS 只處理這件事:

テキスト
IPv6 改變 → Cloudflare AAAA 更新

它不會幫你打開 Router 的 IPv6 入站防火牆。Router 還是要放行 TCP 80 和 443,目的地是 Caddy LXC 的 IPv6:

テキスト
Internet → TCP 443 → IPv6 Firewall → 2001:db8:1234:5678:1a2b:3cff:fe4d:5e6f → Caddy

怎麼判斷是不是防火牆擋住?如果把 IPv6 firewall 關掉網站馬上恢復,打開之後就完全收不到封包,那就是 firewall rule 沒放行。

在 Proxmox 上可以這樣看封包有沒有進來:

Bash
tcpdump -ni veth101i0 'ip6 and tcp port 443'

外部測試時如果一個封包都沒有,先去查 Router 的 IPv6 firewall,不用急著動 Caddy。

反向代理跟 IPv6 是兩回事

我的 Caddy LXC 是 IPv4 192.168.0.2,另外有一個 IPv6。Coder VM 是 192.168.0.38。流程是:

IPv6 IPv4 reverse proxy 外部使用者 Caddy 192.168.0.38Coder VM

外部只要連得到 Caddy 的 IPv6 就行,Caddy 再用內網 IPv4 連到 Coder VM。所以 Coder VM 不需要自己有 public IPv6。

最後的設定

我這種「IPv6 動態、Caddy 當反向代理、Cloudflare 管 DNS」的環境,最後是這樣:

テキスト
{
    dynamic_dns {
        provider cloudflare {env.CLOUDFLARE_API_TOKEN}

        domains {
            sao-x.com
        }

        dynamic_domains

        versions ipv6
        ip_source interface eth0
    }

    email only@sao-x.com

    metrics {
        per_host
    }

    log {
        output stdout
        format json
        level INFO
    }

    order crowdsec first

    crowdsec {
        api_url http://127.0.0.1:8080
        api_key YOUR_CROWDSEC_API_KEY
        ticker_interval 15s
    }
}

(crowdsec_check) {
    log
    crowdsec
}

import /etc/caddy/sites/*

跑一遍:

Bash
caddy validate --config /etc/caddy/Caddyfile
systemctl reload caddy
dig AAAA coder.sao-x.com @1.1.1.1

dig 的結果指向目前 Caddy 的 IPv6,就搞定了。

整條流程長這樣:

caddy-dynamicdns 偵測到變化 reverse_proxy ISP 改了 IPv6 Router Caddy eth0 Cloudflare API AAAA: coder.sao-x.com Caddy 192.168.0.38

之後 ISP 就算把 2001:db8:1234:5678:1a2b:3cff:fe4d:5e6f 換成別的,只要新位址出現在 eth0 上,Dynamic DNS 就會偵測到並更新 Cloudflare 的 AAAA。不用等到網站真的斷了才來處理。

dynamic_dns 只管 DNS 更新

它不會幫你:

  • 改 Router 的 IPv6 firewall
  • 改 Proxmox firewall
  • 轉發 TCP 80/443
  • 改 Caddy 的 reverse proxy
  • 給內部 VM 配 IPv6

整個 IPv6 架構還是得自己確認好:Router 有正常發 IPv6 prefix、Caddy LXC 拿得到 global IPv6、Router 防火牆放行 TCP 80/443、Cloudflare AAAA 指向 Caddy、Caddy 的 reverse proxy 指向內部服務。最後這個 AAAA 在 IPv6 變動時,交給 Dynamic DNS 去更新。

參考

丙午年 寒露

唯一 書

コメント欄を読み込み中...