回源鑑別與源站防護
如何保護源站(自定義回源 Header、回源 IP 白名單),以及多源站時的源站擇優(每節點探測延遲後自動選路)設定。
接入 CDN 後,所有訪客流量都先打到 CDN 節點,節點再「回源」到你的源站取資料。若源站仍對全網開放,攻擊者只要拿到源站真實 IP,就能繞過 CDN(與 WAF / 緩存 / 防護)直連源站。
本文說明兩種讓源站「只信任 FurCDN 回源」的方式:
| 方式 | 原理 | 防偽造 | 推薦度 |
|---|---|---|---|
| 自定義回源 Header | 節點回源時注入一個只有你和 CDN 知道的密鑰 header,源站校驗 | ✅ 強(密鑰不公開) | ⭐ 推薦 |
| 回源 IP 白名單 | 源站防火牆只放行 CDN 節點 IP | ⚠️ 較弱(IP 公開可查,且需定期同步) | 補充手段 |
兩種可疊加
最穩的做法是兩者一起用:防火牆先用 IP 白名單擋掉絕大多數掃描流量,再用自定義 Header 做最終身份校驗。但若只能選一個,優先用自定義 Header —— IP 會變、會被別人查到,Header 密鑰不會。
方式一:自定義回源 Header(推薦)
原理
你在域名設定裡加一條只有你知道的密鑰 header(例如 X-Origin-Secret: 一串隨機字串)。CDN 節點每次回源都會自動帶上它,源站只要檢查這個 header 是否存在且值正確,就能確認「這個請求確實來自 FurCDN,不是有人直連」。
因為密鑰不公開、不出現在任何對外 API,攻擊者無從偽造,所以比 IP 白名單可靠得多。
設定步驟
-
自己生成一串夠長的隨機密鑰,例如:
openssl rand -hex 32 # 例:b3f1c9a7e2d84f06a1c5e9b7d3f0a2c8... -
進入 儀表板 > 域名管理 > 編輯域名 > 回源設定,找到「回源自定義 Header」區塊,點「新增」:
- Name:
X-Origin-Secret(名稱自取,但避開Host/Authorization/Date/X-Amz-Date等保留名稱) - Value:上一步生成的隨機密鑰
- Name:
-
儲存。節點會在後續所有回源請求注入此 header(最多可加 5 條)。
源站校驗範例
只放行帶正確密鑰的請求,其餘一律拒絕。
Nginx:
server {
listen 80;
server_name origin.example.com;
if ($http_x_origin_secret != "你的密鑰") {
return 403;
}
# ... 你的正常配置
}Apache(.htaccess / vhost):
SetEnvIf X-Origin-Secret "^你的密鑰$" from_cdn
Require env from_cdnPHP:
<?php
if (($_SERVER['HTTP_X_ORIGIN_SECRET'] ?? '') !== '你的密鑰') {
http_response_code(403);
exit('Forbidden');
}Caddy:
origin.example.com {
@noSecret not header X-Origin-Secret "你的密鑰"
respond @noSecret 403
reverse_proxy 127.0.0.1:8080
}密鑰保密
這個密鑰等同於源站的「門禁卡」。不要寫進公開 repo、不要洩漏到日誌。懷疑外洩時,在域名設定改一個新值即可(節點下次回源立即生效)。
方式二:回源 IP 白名單
如果你的源站防火牆不方便校驗 header,可以改用 IP 白名單:只放行 FurCDN 節點的回源 IP。
回源 IP 公開 API
平台提供一個公開、免鑑權的端點,列出當前全部節點的回源 IP:
GET https://cdn.taipei/api/public/origin-ips預設回傳 text/plain,一行一個 IP,方便直接灌進防火牆腳本:
curl -s https://cdn.taipei/api/public/origin-ips1.2.3.4
5.6.7.8
9.10.11.12也支援 JSON:
curl -s "https://cdn.taipei/api/public/origin-ips?format=json"{ "ips": ["1.2.3.4", "5.6.7.8", "9.10.11.12"], "count": 3 }這份清單不是固定的
節點會隨時新增、下線、更換 IP,沒有固定的回源 IP 或 IP 段。請不要把它寫死到防火牆,而要定期(建議每 5~10 分鐘)重新拉取並同步規則。否則節點換 IP 後,你的源站會把新節點的回源當成攻擊擋掉,導致網站打不開。
清單已涵蓋 Anycast 節點的 unicast 回源出口 IP。
Anycast 出口可能有多個、且未必即時出現在 API
同一個 Anycast 節點在底層可能有多條 unicast 回源出口(跨 upstream / 跨 PoP 的備援線路),新出口啟用後不保證馬上同步到這支公開 API,可能有數分鐘~數十分鐘的落差。
這代表:單靠 IP 白名單一定會誤報(把合法回源當攻擊擋掉、觸發 403/連線重置,用戶側看起來就是「網站間歇性打不開」)。
強烈建議**務必配合方式一的自定義回源 Header**做身份校驗,IP 白名單只當成第一層粗篩,不要當成唯一鑑別依據。
範例:定期同步到防火牆
ipset + iptables(Linux)—— 放進 cron 每 5 分鐘跑一次:
#!/bin/bash
set -euo pipefail
# 建立 / 確保 ipset 存在
ipset create furcdn hash:ip -exist
ipset create furcdn_new hash:ip -exist
ipset flush furcdn_new
# 拉取最新回源 IP 填入暫存集合
while read -r ip; do
[ -n "$ip" ] && ipset add furcdn_new "$ip" -exist
done < <(curl -fsS https://cdn.taipei/api/public/origin-ips)
# 原子替換,避免同步空窗期把節點擋在外面
ipset swap furcdn_new furcdn
# 首次需掛一條規則(只放行 furcdn 集合 + 本機,其餘拒絕 80/443)
# iptables -A INPUT -p tcp -m multiport --dports 80,443 -m set --match-set furcdn src -j ACCEPT
# iptables -A INPUT -p tcp -m multiport --dports 80,443 -j DROPNginx allow/deny(生成片段後 reload):
#!/bin/bash
set -euo pipefail
OUT=/etc/nginx/furcdn-allow.conf
{
curl -fsS https://cdn.taipei/api/public/origin-ips | sed 's/^/allow /; s/$/;/'
echo "deny all;"
} > "$OUT.tmp" && mv "$OUT.tmp" "$OUT"
nginx -t && nginx -s reload在 server {} 內 include /etc/nginx/furcdn-allow.conf; 即可。
IP 白名單的局限
回源 IP 是公開可查的,任何人都能拿到這份清單。所以 IP 白名單只能擋「不知道你源站 IP 的隨機掃描」,擋不住刻意偽造來源的攻擊者(雖然偽造 TCP 來源 IP 並不容易)。要真正鑑別身份,請搭配方式一的自定義 Header。
該選哪個?
- 大多數人:用自定義 Header 就夠了,設定一次、源站校驗一次,密鑰不變就一勞永逸。
- 有防火牆運維能力、想多一層:自定義 Header 疊加 IP 白名單(定期同步腳本)。
- 不要只靠 IP 白名單且把 IP 寫死 —— 節點換 IP 時網站會掛。
源站擇優(多源站自動選路)
一個網域可以設定 1 個主源站 + 多個備援源站,預設的 failover 策略是「照 origins 陣列順序打,失敗才換下一個」。問題是:對每個節點而言,陣列裡的第一個源站未必最快 —— 香港節點打香港源站可能只要 10ms,打美西源站卻要 150ms。
源站擇優讓每個節點從自己的網路位置對各源站做延遲探測,各自挑出對自己最快的源站。這跟「master 集中決定一個最佳源站」不同:節點分散在全球,只有節點自己才知道哪條路徑最快。
設定存在網域的 originPolicy(一個 JSON 物件):
failover(預設)=維持現行行為,不啟用擇優。latency=依探測分數擇優,這是最常用的設定。- 其餘策略(
round_robin/weighted/random/ip_hash)不需要探測也能運作;probeAlways可讓它們「只觀測、不影響選路」。
為什麼用 ICMP
ICMP echo(ping)是最輕量的 RTT 量測方式:不需要源站跑任何應用層邏輯、不佔用 HTTP 連線、每輪只送幾個封包。節點每輪對每個源站送 probeCount 個封包,取平均 RTT 當作該輪分數,丟包率則用來懲罰。
若環境不允許 ICMP,也可以改用 TCP connect 時間(probeMethod: tcp)或 HTTP HEAD(probeMethod: http)。
選路策略
| strategy | 行為 |
|---|---|
failover | 預設。依 origins 陣列順序嘗試,失敗才換下一個;backup: true 的源站一律排到最後。 |
latency | 依最近 scoreWindow 輪的探測分數擇優(分數越低越好)。啟用探測。 |
round_robin | 輪流使用所有非備用源站,weight 影響分配比例。 |
weighted | 依每個源站的 weight 加權隨機挑選。 |
random | 隨機挑一個非備用源站。 |
ip_hash | 依 hashKey(client_ip 或 uri)雜湊,把同一個訪客(或同一個 URI)固定導到同一個源站,藉此提高源站快取命中。 |
備用源站(backup)
不論哪一種策略,backup: true 的源站都只在「所有非備用源站都不可用」時才會被使用,適合放只在災難時才啟用的冷備援。
完整選項(originPolicy)
所有欄位都有預設值,只需要帶要改的欄位:未帶的欄位沿用目前設定(伺服器淺合併後整體驗證),全新網域則套預設。
| 欄位 | 型別 / 範圍 | 預設 | 說明 |
|---|---|---|---|
strategy | failover / latency / round_robin / weighted / random / ip_hash | failover | 選路策略,見上表 |
probeMethod | icmp / tcp / http | icmp | 探測方式 |
probeFallback | bool | true | ICMP 不可用(無 raw socket 權限,或目標徹底擋掉 ICMP)時,退回用 TCP connect 時間 |
probeIntervalSec | 整數 5~3600 | 15 | 每輪探測間隔秒數 |
probeTimeoutMs | 整數 200~10000 | 2000 | 單一探測封包的逾時毫秒 |
probeCount | 整數 1~10 | 3 | 每輪送幾個封包;RTT 取該輪平均,丟包率為該輪遺失比例 |
probePort | 整數 1~65535 或 null | null | TCP/HTTP 探測埠;null 表示依 URL scheme(http→80、https→443)或 URL 內指定的埠 |
probePath | 以 / 開頭的字串,≤256 字元 | / | HTTP 探測路徑(送 HEAD,回應 <500 視為成功) |
probeAlways | bool | false | 非 latency 策略時也持續探測並上報(只用於觀測,不影響選路) |
scoreWindow | 整數 1~30 | 5 | 分數=最近 N 輪 RTT 的平均 |
lossPenaltyMs | 整數 0~5000 | 1000 | 分數公式:avgRtt + 丟包率(0~1) × lossPenaltyMs + origin.latencyBiasMs |
maxLatencyMs | 整數 0~30000 | 0 | 大於 0 時,分數高於此值的源站會被排到最後(仍保留在 failover 鏈中,不會被剔除) |
switchThresholdMs | 整數 0~5000 | 20 | 遲滯:新最優者必須比目前選中的源站好至少這麼多毫秒才切換 |
minHoldSec | 整數 0~3600 | 30 | 切換後至少維持這麼久才允許再次切換 |
hashKey | client_ip / uri | client_ip | ip_hash 策略使用的雜湊鍵 |
retryStatuses | 整數陣列(400~599,最多 20 個) | [] | 空陣列=所有 5xx 都觸發換源;非空=只有清單內的狀態碼才觸發 |
retryOnError | bool | true | 連線失敗/逾時是否觸發換源 |
maxAttempts | 整數 0~10 | 0 | 單一請求最多嘗試幾個源站;0=不限(最多嘗試所有源站) |
每個源站的選項
除了 URL / 類型 / SNI / Host override 之外,origins[] 陣列裡的每個源站另外支援:
| 欄位 | 範圍 | 預設 | 說明 |
|---|---|---|---|
weight | 整數 1~1000 | 1 | weighted / round_robin 的權重 |
backup | bool | false | true=備用:只在所有非備用源站都不可用時才使用(任何策略皆然) |
probeHost | hostname / IP 或空字串 | "" | ICMP/TCP 探測的目標;空=用源站 URL 的 host。與源站 URL 適用同等的內網 / 保留位址檢查(非管理員不得指向私網) |
latencyBiasMs | 整數 -1000~10000 | 0 | 加到分數上的固定偏移:正值=懲罰、負值=偏好 |
probeDisabled | bool | false | 不探測此源站;latency 策略下會排在有分數的源站之後 |
探測與 ICMP 注意事項
源站與中間防火牆需放行 ICMP
節點是從自己的網路位置直接對源站送 ICMP echo。如果源站主機或中間的雲防火牆 / 安全組擋掉 ICMP,探測會全部逾時。
此時若 probeFallback: true(預設),節點會自動退回量測 TCP connect 時間;若 TCP 也不通(埠沒開、防火牆擋),該源站會被視為不可用。建議:
- 在源站安全組放行 CDN 節點的 ICMP(來源可用上方「方式二」的回源 IP 公開 API 取得);
- 或直接把
probeMethod設成tcp(例如源站只開 443 時)。
- 剛啟動、還沒有探測資料時,節點會先依配置順序選路,等第一輪探測完成(預設 15 秒)後才切換到擇優結果。
- 設定變更後(新增 / 修改源站),節點會立即對受影響的源站補跑一輪探測,不必等下一輪間隔。
- 被判定為故障的源站會被既有健康檢查機制暫時排除;若全部源站都被判定故障,則 fail-open(照樣嘗試),避免整體 502。
遲滯(hysteresis):避免源站一直換來換去
延遲是會抖動的。如果只要「B 比 A 快 0.5ms」就切換,兩個源站分數接近時會不停來回切,反而製造更多回源連線與快取碎片。因此擇優加了兩道剎車:
switchThresholdMs(預設 20ms):新最優者必須比目前選中者好至少這麼多才切換。minHoldSec(預設 30 秒):切換後至少維持這麼久,期間不再切。
範例:目前選中源站 A,分數 45.0ms。
| 情境 | 候選 B 分數 | 差距 | 結果 |
|---|---|---|---|
| 1 | 40.0ms | 5ms < 20ms | 維持 A(差距不足門檻) |
| 2 | 22.0ms | 23ms ≥ 20ms | 切換到 B,並在接下來 30 秒內不再切換 |
| 3 | 兩者分數都高於 maxLatencyMs(若設為 3000) | — | 兩者都被排到最後,但仍按分數排序 |
存取日誌欄位
啟用擇優後,每筆存取日誌會多帶:
| 欄位 | 說明 |
|---|---|
originUrl | 最終實際回應的源站 URL(已遮蔽 userinfo) |
originRttMs | 該源站在當下的探測分數(毫秒);沒有探測資料時省略 |
originStrategy | 這次請求使用的策略 |
originTrace 會在最前面帶上策略與候選排序,例如:
擇優(icmp) 源站2 12.3ms > 源站1 45.0ms | 源站2 http://b → HTTP 200意思是:這次用 ICMP 擇優,候選排序為「源站2(12.3ms)優於源站1(45.0ms)」,最終由源站2 回應,HTTP 200。
透過 API 設定
擇優設定屬於登入後的 dashboard API(session 鑑權,與 CLI furcdn login 用的是同一套),不在對外開放的 /api/v1/** API key 端點內。
# 讀取目前設定(GET /api/domains 回傳 { domains: [...] },每筆含 originPolicy)
curl -s https://cdn.taipei/api/domains \
-H "Cookie: furcdn_token=<session>" | jq '.domains[] | select(.id==123) | .originPolicy'
# 只改 strategy 等欄位(伺服器會淺合併既有設定後整體驗證)
curl -s -X PUT https://cdn.taipei/api/domains/123 \
-H 'content-type: application/json' \
-H 'Cookie: furcdn_token=<session>' \
-d '{"originPolicy":{"strategy":"latency","probeMethod":"icmp","switchThresholdMs":20}}'
# 重置為全部預設
curl -s -X PUT https://cdn.taipei/api/domains/123 \
-H 'content-type: application/json' \
-H 'Cookie: furcdn_token=<session>' \
-d '{"originPolicy":null}'
# 各節點最近一輪的探測結果(RTT / 丟包 / 該節點選中的源站)
curl -s https://cdn.taipei/api/domains/123/origin-probes \
-H "Cookie: furcdn_token=<session>"origin-probes 的回應格式:
{
"nodes": [
{
"nodeId": 7,
"nodeName": "hk-1",
"ts": 1735689600000,
"origins": [
{ "url": "http://a.example", "method": "icmp", "rttMs": 12.3, "lossPct": 0, "ok": true, "selected": true, "ts": 1735689600000 },
{ "url": "http://b.example", "method": "icmp", "rttMs": 45.0, "lossPct": 0, "ok": true, "selected": false, "ts": 1735689600000 }
]
}
]
}超過 15 分鐘沒有回報的節點不會出現在清單中。
透過 CLI 設定
# 啟用擇優(只改 strategy,其餘沿用預設)
furcdn domain origin policy set example.com --set strategy=latency
# 或等價的旗標寫法
furcdn domain update example.com --origin-strategy latency --probe-method icmp --switch-threshold 20
# 檢視生效中的完整設定(未自訂的欄位標為「預設」)
furcdn domain origin policy get example.com
# 調整單一源站:權重、備用、探測目標、不探測
furcdn domain origin set example.com --url http://b.example --backup --weight 3 --probe-host 1.2.3.4
furcdn domain origin set example.com --url http://c.example --latency-bias -50 --no-probe
furcdn domain origin list example.com
# 看各節點的探測結果(表格 / --json)
furcdn domain origin probes example.com
# 重置為全部預設
furcdn domain origin policy set example.com --reset排查
- 一直只走同一個源站:確認
strategy已是latency(預設是failover),且該網域有多個非備用源站。 - 探測全部逾時 / 分數都是空的:先確認源站放行 ICMP;不放行就把
probeMethod改成tcp。 - 源站一直換來換去:調高
switchThresholdMs(例如 50)與minHoldSec(例如 120)。 - 更多症狀見「常見問題排查」。