# 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
```