---
# System prepended metadata

title: DrayTek Vigor IPv6 防火牆與 Cloudflare DDNS：避免 Temporary IPv6 更新到 DNS 的排查紀錄

---

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