跳轉到

OONI 怎麼判定一個網站被封鎖

OONI 測量資料結構導覽 說明了欄位放在哪裡,接下來的問題是欄位值怎麼算出來。看到 blocking: "dns" 時,多數情況還不足以斷定某個網站在台灣被封鎖。

blocking 只記錄單次測量與對照組不一致,距離「確認封鎖」還有幾步。以下拆解判定機制、四種類型各自對應的證據,以及把單筆測量推到可信結論需要補上什麼。

判定的基礎:雙邊對照

Probe 單獨測一個網站,無法區分「網站遭到干預」與「網站本身無法連線」。web_connectivity 的作法是同一個網址測兩次,一次從 Probe 所在的網路,一次請 OONI 的 test helper 從外部網路測,再比對兩邊結果。

test helper 是 OONI 架設在外部網路的測量伺服器,觀測結果收在 test_keys.control 底下,包含 DNS 解析結果、TCP 連線狀態與 HTTP 回應。判定欄位全部建立在雙邊差異上:

差異出現的位置 判定
DNS 解析結果不一致 blocking: "dns"
DNS 一致,Probe 連不上但 test helper 連得上 blocking: "tcp_ip"
TCP 連得上,HTTP 階段失敗 blocking: "http-failure"
HTTP 有回應,內容與 test helper 取得的不同 blocking: "http-diff"
兩邊都正常且內容相符 blocking: falseaccessible: true

判定順序由前往後,DNS 階段就出問題時不會繼續往下比對。四個 *_match 欄位在前三種情況下全是 null,原因正是比對在更早的階段就中止。

先看 control 再看 blocking

blocking 是雙邊比對的結果,不是 Probe 單方面的觀測。判讀任何一筆異常測量時,先確認 test_keys.control 裡 test helper 看到什麼,再回頭讀 blocking。下一節的 tcp_ip 範例會示範,忽略 control 會把網站自身的問題誤判成封鎖。

四種判定對應的證據

後續章節引用的五筆測量都是 2026-08-04 的公開資料,可在 OONI Explorer 查到原始內容。

dns:解析結果不一致

台灣 https://ntc.party/ 的測量,Probe 對 A 與 AAAA 的查詢都回報 dns_nxdomain_error(網域不存在),test helper 端查得到,因此 dns_consistency 標為 inconsistent

test_keys.queries
{"query_type": "A", "failure": "dns_nxdomain_error", "answers": []}
{"query_type": "AAAA", "failure": "dns_nxdomain_error", "answers": []}

DNS 判定要留意解析器歸屬。該筆的 resolver_asnAS13335(Cloudflare),probe_asn 則是 AS3462(中華電信)。測量者把 DNS 指向境外服務時,DNS 階段的異常未必反映本地電信商的行為。

tcp_ip:連不到目標位址

台灣 http://www.tkec.com.tw/ 的測量判定為 tcp_ip,DNS 正常解析到 210.64.193.1,Probe 連 80 埠逾時。

只看 Probe 端容易誤讀成封鎖,對照 control 的結果就會得到不同結論:

test_keys.control.tcp_connect
{"210.64.193.1:443": {"status": true,  "failure": null},
 "210.64.193.1:80":  {"status": false, "failure": "generic_timeout_error"}}

test helper 從外部連 80 埠同樣逾時,代表該網站的 80 埠從外部一樣連不上,與台灣的網路環境無關。同一個 IP 的 443 埠兩邊都通,更說明問題出在該埠而非路徑。

http-failure:連得上但取不到內容

台灣 https://bit.ly/ 的測量判定為 http-failure。DNS 正常,兩個目標 IP 的 443 埠都連得上,control 顯示 test helper 端也一樣,差異出現在 HTTP 階段的 generic_timeout_error

連線層沒問題而應用層失敗,常見於伺服器端的速率限制、對特定來源的拒絕服務,以及 TLS 層的中斷。要區分成因需要看 tls_handshakesnetwork_events 的時序。

http-diff:取得的內容不一致

台灣的資料裡有 http-diff,但樣態與封鎖告示頁不同(見文末台灣現況),以下改用印尼的測量說明典型的告示頁長什麼樣。

四種類型裡只有 http-diff 會讓四個 *_match 欄位派上用場。印尼 http://www.sportsinteraction.com/ 的測量是典型例子:

觀測方 狀態碼 標題 內容長度
Probe 200 Trustpositif 7,044
test helper 403 Maintenance 7,067

Probe 的請求被重導向到 http://lamanlabuh.aduankonten.id/,落在印尼官方的內容申訴網域,頁面標題 Trustpositif 是當地網路內容過濾系統的名稱。ISP 在連線途中把使用者導向告示頁,是 http-diff 最典型的成因。

該筆同時示範了單一欄位不可靠:

四個 *_match 欄位與 body_proportion
title_match: false        status_code_match: false
headers_match: true       body_length_match: true
body_proportion: 0.9967

body_length_matchtruebody_proportion 逼近 1,純粹因為封鎖告示頁與對照組頁面的長度剛好接近。只看長度會得到錯誤結論,四個欄位要一起讀,其中 title_matchstatus_code_match 的鑑別力通常最高。

誤判從哪裡來

blocking 有值而實際上沒有網路干預,是資料集裡的常態。前面 tkec.com.tw80 埠不通已經是一例,再看一筆更隱蔽的。

埃及 http://www.newipnow.com/ 的測量判定為 http-diff,四個 *_match 有三個不符,body_proportion 只有 0.02。看起來像被插入封鎖頁,實際內容是:

觀測方 狀態碼 標題
Probe 520 newipnow.com \| 520: Web server is returning an unknown error
test helper 200 Buy Private Proxies: $0.88 per Dedicated Premium IP - NewIPNow

520 是目標網站前方的 Cloudflare 在來源伺服器異常時回應的錯誤頁,代表該網站當下發生異常,與埃及的網路管制無關。另外值得留意的是,該筆的 probe_asnAS13335(Cloudflare),測量端本身也走在 Cloudflare 的網路上,屬於下面第二類的測量環境因素。

常見的誤判來源可以歸成幾類:

  • 來源網站自身狀態:伺服器錯誤、維護頁、CDN 錯誤頁、地理限制。
  • 測量環境:Probe 開著 VPN 或 Tor 時,測到的是出口所在地的網路。
  • 網站的動態內容:輪播廣告、個人化內容、A/B 測試會讓兩邊的內容本來就不同。
  • 對照組本身失敗control_failure 有值時,雙邊比對的前提不成立。

OONI 在資料集層級也處理同一類問題,作法可參考 OONI 如何分辨壞掉的量測資料,該文整理了 OONI 用哪些啟發式規則過濾異常投稿。

從單筆測量到可信結論

要把觀測推進到「某個網站在某地被封鎖」,單筆資料不夠。實務上補上幾個維度:

  1. 跨時間:同一個網址在數天到數週內反覆出現同樣的判定,才能排除偶發故障。
  2. 跨 ASN:多家電信商都測到相同結果,指向網路層的普遍行為。只在單一 ASN 出現,較可能是該業者的個別設定。
  3. 跨解析器:換不同 DNS 解析器仍然異常,才能排除解析器自身問題。
  4. confirmed 欄位:OONI 後端會用已知的封鎖告示頁指紋比對測量,命中時把 confirmed 標為 true。該欄位在 measurements API 的回應中,屬於後端分析結果,不在 Probe 產生的原始資料裡。

想自己執行跨時間或跨 ASN 的比對,ASN 觀測資料擷取與分析 有批次取用的作法。

confirmedtrue 不等於 http-diff

兩者容易混淆。confirmed 標記的依據是封鎖指紋比對,DNS 被導向已知的封鎖用 IP 也會標記為 confirmed,判定類型仍是 dns。撰稿時抽查伊朗、俄羅斯、土耳其、中國各 5 筆 confirmed 測量,樣本全部落在 blocking: "dns"。樣本數少,僅能說明兩者並非同一件事,不足以推論一般規律。

台灣現況

以下數字取自 2026-08-05 往前 24 小時的台灣 web_connectivity 全量資料,共 22,105 筆測量。統計方式是把該區間 S3 上的每一筆逐行解析,取 test_keys.blocking 依 ASN 累計:

判定 筆數 佔比
false(未觀測到干預) 21,029 95.13%
none(沒有判定結果) 512 2.32%
tcp_ip 312 1.41%
dns 150 0.68%
http-failure 67 0.30%
http-diff 35 0.16%

四種干預類型合計 564 筆,佔全部測量的 2.55%。none 是缺 test_keysblockingnull 的測量,做異常率統計時要先決定放進分母或排除。

另外抽查 100 筆異常測量,confirmedtrue 的有 0 筆,與 ASN 自治網路觀測資料分析 長期以來的描述一致。

小樣本會給出錯誤的分布

本頁初稿曾以 measurements API 異常清單的 40 筆抽樣估計判定分布,得到 dns 最多、http-diff 掛零的結論。改用全量統計後兩點都不成立:實際上 tcp_ipdns 的兩倍以上,http-diff 也確實存在。API 的異常清單有自己的排序,直接當隨機樣本用會失準。要看分布請走全量統計,作法見 ASN 觀測資料擷取與分析

台灣的 http-diff 與前面印尼的例子不是同一回事。抽查 4 小時區間內的 15 筆,body_proportion 全部落在 0.0040.21,Probe 取到的內容遠短於對照組,與封鎖告示頁那種逼近 1 的樣態相反。內容極短通常指向錯誤頁或空回應,成因需要逐筆查驗證據層才能判斷,本頁不下結論。

要自己重新執行上面的統計:

取得判定分布
uv run python ooni.py lookback --units=24 --loc=TW --frame=hours

指令會印出 blocking 的合計,並把逐 ASN 的分布寫進 CSV。單筆查驗仍走 API:

列出台灣的異常測量
curl -s "https://api.ooni.io/api/v1/measurements?probe_cc=TW&test_name=web_connectivity&anomaly=true&limit=100" \
  | python3 -m json.tool | head -40

以上是單一 24 小時區間的快照,反映的是當日樣態,不足以當作長期趨勢的結論。要看趨勢需要涵蓋更長時間,而台灣的觀測本身就存在 ASN 集中度過高的限制,細節見前面提到的 ASN 自治網路觀測資料分析

延伸閱讀

想自己執行跨時間、跨 ASN 的比對,從擷取指南開始。

判定演算法的完整定義在上游 ts-017-web-connectivity,失敗字串的統一命名在 df-007-errors