# DrayTek Vigor IPv6 防火牆與 Cloudflare DDNS:避免 Temporary IPv6 更新到 DNS 的排查紀錄 ## 環境 - Router:DrayTek Vigor 系列 - Server:Linux 主機,Ubuntu / Debian 類似環境 - DDNS:`favonia/cloudflare-ddns` - DNS Provider:Cloudflare - 目標: - 對外開放 IPv6 HTTPS 443 - 防火牆只允許特定 Server 的 IPv6 - 不讓整個 LAN 暴露在 IPv6 inbound 下 - 保留 Linux 主機 outbound temporary IPv6 的隱私特性 - Cloudflare AAAA record 不要更新到 temporary IPv6 --- ## 1. 問題起點 原本想把家中或實驗室內的一台 Linux Server 透過 IPv6 對外提供 HTTPS: ```text https://service.example.com ```` 一開始直覺以為: ```text 在 Router 上允許 TCP 443 外部應該就能連進來 ``` 但實際情況是: ```text IPv6 outbound 正常 Router 自己可以 ping IPv6 Server 也有 Global IPv6 但外部連 service.example.com:443 不穩或不通 ``` 後來確認問題不是單一點,而是三件事疊在一起: ```text 1. IPv6 inbound 不是 IPv4 NAT port forwarding 2. 防火牆要允許的是 Server 的 stable IPv6 /128 3. DDNS 一開始更新到了 temporary IPv6 ``` --- ## 2. IPv6 inbound 和 IPv4 NAT 的差異 IPv4 常見做法是: ```text Internet → Router public IPv4:443 → NAT / Port Forward → 192.168.x.x:443 ``` IPv6 一般不是這樣。 IPv6 裡,內部 Server 會直接擁有自己的 Global IPv6。外部 client 要連的是 Server 本身的 IPv6,而不是 Router 的 WAN IPv6: ```text Internet → Server Global IPv6:443 ``` Router 的角色是做防火牆,不是做 NAT: ```text 允許: WAN → LAN 某一台 Server 的 TCP 443 阻擋: 其他 WAN 主動進入 LAN 的 IPv6 routed connection ``` Cloudflare 的 AAAA record 也是用來把 domain name 指到 IPv6 address;AAAA record 對 IPv6 的角色,類似 A record 對 IPv4 的角色。Cloudflare 文件也說明 A / AAAA records 會把 domain name 對應到一個或多個 IPv4 / IPv6 位址。 參考:Cloudflare DNS record types:[https://developers.cloudflare.com/dns/manage-dns-records/reference/dns-record-types/](https://developers.cloudflare.com/dns/manage-dns-records/reference/dns-record-types/) --- ## 3. Vigor 防火牆規則重點 Vigor 防火牆的核心設定方向: ```text Data Filter:Enable Start Filter Set:建議使用實際 Data Filter 的起始組,例如 Set #2 Block routing packet from WAN: IPv6:Enable Strict Security Firewall:Enable ``` 允許 HTTPS 的規則應該類似: ```text Direction:WAN → LAN/DMZ/RT/VPN Source:Any Destination:Server stable IPv6 /128 Service:TCP 443 Action:Pass Immediately Log:Enable ``` 重點是: ```text 允許 443 的規則要放在前面 阻擋 WAN inbound 的規則要放在後面 ``` 因為防火牆規則通常會由上往下比對,先命中的規則會先套用。 --- ## 4. 不要用 MAC 物件辨識 IPv6 Server 一開始嘗試用 MAC address 來建立防火牆物件,例如: ```text Object Name:server01 Type:MAC address MAC:AA:BB:CC:DD:EE:FF IPv6:2001:db8:1234:5678:.... ``` 看似是在描述「這台機器」,但在 IPv6 防火牆規則中,真正應該比對的是 IP address。 IPv6 主機可能同時有多個位址: ```text link-local IPv6 ULA IPv6 stable global IPv6 temporary global IPv6 ``` 所以用 MAC 來當 IPv6 firewall destination identity 並不可靠。 後來改成明確的 IPv6 address object: ```text Object Type:IPv6 Address Type:Single Address Address:2001:db8:1234:5678:1111:2222:3333:4444 Prefix Length:128 ``` 也就是: ```text 2001:db8:1234:5678:1111:2222:3333:4444/128 ``` 修正後,Router firewall 才能精準允許: ```text WAN → LAN TCP 443 → 這台 Server 的 stable IPv6 ``` --- ## 5. Linux 上看到 stable IPv6 與 temporary IPv6 在 Server 上查 IPv6: ```bash ip -6 addr show ``` 可能會看到類似: ```text inet6 2001:db8:1234:5678:1111:2222:3333:4444/64 scope global dynamic mngtmpaddr inet6 2001:db8:1234:5678:aaaa:bbbb:cccc:dddd/64 scope global temporary dynamic ``` 這裡有兩個重點: ```text stable IPv6: 2001:db8:1234:5678:1111:2222:3333:4444 temporary IPv6: 2001:db8:1234:5678:aaaa:bbbb:cccc:dddd ``` RFC 4941 的 IPv6 Privacy Extensions 會讓主機產生會隨時間改變的 temporary address,目的在降低用固定 interface identifier 追蹤使用者的可能性。 參考:RFC 4941:[https://datatracker.ietf.org/doc/html/rfc4941](https://datatracker.ietf.org/doc/html/rfc4941) 因此: ```text temporary IPv6 適合 client outbound privacy stable IPv6 適合 server inbound endpoint ``` --- ## 6. 為什麼 temporary IPv6 不適合當 Server 對外位址 如果把 DNS AAAA 指到 temporary IPv6,會有幾個問題: ```text 1. temporary IPv6 會過期或輪替 2. Router firewall 若只允許 stable IPv6 /128,temporary IPv6 不會被放行 3. 外部 client 可能解析到舊的 temporary IPv6 4. 服務會變成時好時壞 ``` Server 對外服務需要的是穩定 endpoint: ```text DNS AAAA → stable IPv6 Firewall → allow stable IPv6 /128 Service → listen on IPv6 443 ``` Temporary IPv6 應該保留給一般 outbound 連線使用。 --- ## 7. DDNS 為什麼會更新到 temporary IPv6 原本 `favonia/cloudflare-ddns` 設定大概是: ```yaml services: cloudflare-ddns: image: favonia/cloudflare-ddns:latest container_name: cloudflare-ddns network_mode: host restart: unless-stopped environment: - CLOUDFLARE_API_TOKEN=REDACTED - DOMAINS=service.example.com - IP4_PROVIDER=cloudflare.doh - IP6_PROVIDER=cloudflare.doh - UPDATE_CRON=@every 5m ``` 問題在: ```yaml - IP6_PROVIDER=cloudflare.doh ``` 這類 provider 會用外部服務偵測「目前連出去時看到的 public IP」。如果 Linux kernel 選了 temporary IPv6 當 outbound source address,那 DDNS updater 取得的 IPv6 就會是 temporary IPv6。 `favonia/cloudflare-ddns` 文件說明 `IP4_PROVIDER` 控制 A record / IPv4,`IP6_PROVIDER` 控制 AAAA record / IPv6,兩者可以分開指定不同 provider。 參考:favonia/cloudflare-ddns:[https://github.com/favonia/cloudflare-ddns](https://github.com/favonia/cloudflare-ddns) --- ## 8. 解法:IPv6 使用 file provider,自己提供 stable IPv6 `favonia/cloudflare-ddns` 支援 `file:` provider,可從本機檔案讀取 IP,並在每次偵測週期重新讀取。這很適合用腳本把「非 temporary 的 stable IPv6」寫進檔案。 參考:favonia/cloudflare-ddns releases:[https://github.com/favonia/cloudflare-ddns/releases](https://github.com/favonia/cloudflare-ddns/releases) 目錄範例: ```text /opt/cloudflare-ddns/ ├── docker-compose.yml ├── data/ │ └── ip6.txt └── scripts/ └── update-ip6.sh ``` 建立目錄: ```bash cd /opt/cloudflare-ddns mkdir -p scripts data ``` 建立腳本: ```bash nano scripts/update-ip6.sh ``` 內容: ```bash #!/usr/bin/env bash set -euo pipefail IFACE="eth0" OUT="/opt/cloudflare-ddns/data/ip6.txt" ip -6 -o addr show dev "$IFACE" scope global \ | grep -v ' temporary ' \ | grep -v ' deprecated ' \ | awk '{print $4}' \ | cut -d/ -f1 \ | grep -viE '^(fd|fc)' \ | head -n 1 \ > "$OUT" chmod 644 "$OUT" ``` > 注意:`IFACE="eth0"` 請改成實際網卡名稱,例如 `enp3s0`、`ens18`、`eno1` 等。 套權限並測試: ```bash chmod +x scripts/update-ip6.sh ./scripts/update-ip6.sh cat data/ip6.txt ``` 預期輸出: ```text 2001:db8:1234:5678:1111:2222:3333:4444 ``` 不應該是 temporary address。 --- ## 9. 修改 docker-compose.yml 修改後範例: ```yaml services: cloudflare-ddns: image: favonia/cloudflare-ddns:latest container_name: cloudflare-ddns network_mode: host restart: unless-stopped environment: - CLOUDFLARE_API_TOKEN=REDACTED - DOMAINS=service.example.com - IP4_PROVIDER=cloudflare.doh - IP6_PROVIDER=file:/ip6.txt - IP6_DEFAULT_PREFIX_LEN=128 - UPDATE_CRON=@every 5m volumes: - /opt/cloudflare-ddns/data/ip6.txt:/ip6.txt:ro ``` 重點: ```yaml - IP6_PROVIDER=file:/ip6.txt ``` 這裡 `/ip6.txt` 是 container 內看到的路徑。 Volume: ```yaml - /opt/cloudflare-ddns/data/ip6.txt:/ip6.txt:ro ``` 左邊是 host: ```text /opt/cloudflare-ddns/data/ip6.txt ``` 右邊是 container: ```text /ip6.txt ``` 所以 `IP6_PROVIDER` 要寫 container 內的路徑。 --- ## 10. 重啟 DDNS container ```bash cd /opt/cloudflare-ddns docker compose down docker compose up -d docker logs -f cloudflare-ddns ``` 成功時應看到類似: ```text IPv6 provider: file:/ip6.txt Detected IPv6 address: 2001:db8:1234:5678:1111:2222:3333:4444/128 Updated an outdated AAAA record for service.example.com ``` --- ## 11. 清掉 Cloudflare 上舊的 temporary AAAA record 如果之前 DDNS 曾經把 temporary IPv6 寫入 Cloudflare,可能會看到同一 hostname 有多筆 AAAA: ```bash dig AAAA service.example.com +short ``` 輸出可能類似: ```text 2001:db8:1234:5678:aaaa:bbbb:cccc:dddd 2001:db8:1234:5678:1111:2222:3333:4444 ``` 第一筆是舊 temporary,第二筆是 stable。 Cloudflare 允許同一個 hostname 有多筆 A / AAAA record;但 A / AAAA 不能和同名 CNAME 混用。 參考:Cloudflare records with same name:[https://developers.cloudflare.com/dns/manage-dns-records/troubleshooting/records-with-same-name/](https://developers.cloudflare.com/dns/manage-dns-records/troubleshooting/records-with-same-name/) 處理方式: ```text Cloudflare Dashboard → Zone → DNS → Records → 找到 service.example.com → 刪除 temporary IPv6 那筆 AAAA → 只保留 stable IPv6 那筆 AAAA ``` 再確認: ```bash dig @1.1.1.1 AAAA service.example.com +short ``` 預期只剩: ```text 2001:db8:1234:5678:1111:2222:3333:4444 ``` --- ## 12. 自動更新 stable IPv6 檔案 若 ISP prefix 可能變動,`data/ip6.txt` 不應只手動產生一次。可以用 systemd timer 定期更新。 建立 service: ```bash sudo nano /etc/systemd/system/cloudflare-ddns-ip6-file.service ``` 內容: ```ini [Unit] Description=Update stable IPv6 file for Cloudflare DDNS [Service] Type=oneshot ExecStart=/opt/cloudflare-ddns/scripts/update-ip6.sh ``` 建立 timer: ```bash sudo nano /etc/systemd/system/cloudflare-ddns-ip6-file.timer ``` 內容: ```ini [Unit] Description=Refresh stable IPv6 file for Cloudflare DDNS [Timer] OnBootSec=30s OnUnitActiveSec=5min Unit=cloudflare-ddns-ip6-file.service [Install] WantedBy=timers.target ``` 啟用: ```bash sudo systemctl daemon-reload sudo systemctl enable --now cloudflare-ddns-ip6-file.timer ``` 確認: ```bash systemctl list-timers | grep cloudflare-ddns-ip6 cat /opt/cloudflare-ddns/data/ip6.txt ``` --- ## 13. 最終架構 ```text Internet Client → DNS 查 service.example.com → Cloudflare AAAA 回 stable IPv6 → Router IPv6 Firewall → 允許 WAN → LAN TCP 443 到 Server stable IPv6 /128 → Linux Server:443 ``` Server 一般對外連線仍可使用 temporary IPv6: ```text Linux Server outbound → temporary IPv6 → 保留 privacy extension 的優點 ``` 對外服務則使用 stable IPv6: ```text Cloudflare AAAA → stable IPv6 → Router firewall /128 allow rule ``` --- ## 14. 驗證指令 ### 查 Server IPv6 ```bash ip -6 addr show dev eth0 scope global ``` 看有沒有 stable 與 temporary: ```text inet6 2001:db8:... scope global dynamic mngtmpaddr inet6 2001:db8:... scope global temporary dynamic ``` ### 查 DDNS file ```bash cat /opt/cloudflare-ddns/data/ip6.txt ``` ### 查 DNS AAAA ```bash dig @1.1.1.1 AAAA service.example.com +short ``` ### 查 container log ```bash docker logs -f cloudflare-ddns ``` ### 從外部測 IPv6 HTTPS ```bash curl -6 -vk https://service.example.com ``` ### Router 防火牆概念確認 確認 firewall rule 類似: ```text Direction:WAN → LAN Destination:Server stable IPv6 /128 Service:TCP 443 Action:Pass Immediately ``` --- ## 15. 安全注意事項 改用 `.env`: ```env CLOUDFLARE_API_TOKEN=REDACTED ``` Cloudflare API token 若外洩,應立即 roll / revoke / recreate。 參考:Cloudflare Roll API token:[https://developers.cloudflare.com/fundamentals/api/how-to/roll-token/](https://developers.cloudflare.com/fundamentals/api/how-to/roll-token/) 建議權限最小化: ```text Zone - DNS - Edit Zone - Zone - Read 限制在單一 zone ``` --- ## 16. 結論 這次問題的根本原因不是單純「443 沒開」,而是 IPv6 的 server endpoint、privacy extension、DDNS provider 行為與防火牆規則互相影響。 最後穩定解法是: ```text 1. Router firewall 只放行 Server stable IPv6 /128 的 TCP 443 2. Linux Server 保留 temporary IPv6 給 outbound privacy 3. Cloudflare DDNS 的 IPv6 不用 whoami / DoH 偵測 4. 改用 file provider,餵入 stable IPv6 5. 清掉 Cloudflare 上舊的 temporary AAAA record ``` 核心觀念: ```text Temporary IPv6:給 client outbound privacy Stable IPv6:給 server inbound identity Firewall:只允許 stable IPv6 /128 DDNS:只發布 stable IPv6 ```