vatt'ghern jaskier's ballads

兩端 BGP 鄰居只要都跑 RFC 9234,路由外洩理論上就不用人工政策擋——收到的一端看見 OTC 位元就能直接拒收。Cloudflare 攤開自己在全球的 peering session 資料後卻看到:這個位元傳到兩家大型 Tier-1 網路手上,就這樣不見了。

兩家 Tier-1 把防路由外洩的欄位拆掉了

由外洩的定義很簡單:一條路由跑到了它原本不該去的地方。RFC 7908 把這件事寫成正式規格,但寫規格跟真的擋下外洩是兩回事——過去擋外洩幾乎全靠 operator 自己維護的複雜、容易出錯的路由政策,任何一段忘了更新,外洩就直接漏過去。RFC 9234 想解決的正是這個依賴人工的老問題:讓兩端 BGP 鄰居在 session 一開始就先講好彼此的角色,之後路由器自己就能判斷一條路由該不該往上送,不用等人翻政策表。Cloudflare 用自己在全球的 peering 據點去看這套機制實際跑得怎麼樣,方法很直接——盯著鄰居送過來的路由裡,有沒有帶 OTC 這個位元。照理說,只要兩端都懂 RFC 9234,這個位元就該一路跟著路由走。Cloudflare 看到的卻不是這樣。

Cloudflare 自己對「路由外洩」這件事的態度並不是第一次表態。文章開場就先把後果講白:「Route leaks push traffic down paths it was never meant to take」——路由外洩把流量推去一條它本來不該走的路徑,他們自己的說法是「we have written and spoken publicly in the past about route leaks in Border Gateway Protocol (BGP), depicting these events as impactful incidents that cause misdirection of traffic through unintended network paths」。換句話說,這不是他們第一次談這個題目;過去已經公開寫過、講過好幾次外洩事件的實際影響,而這篇文章要問的問題更具體:一個理論上能自動擋下外洩的欄位,實際跑在真實網路上,真的一路活著抵達它該去的地方嗎?

外洩路由靠什麼擋下——RFC 9234 之前的答案

BGP 的路由傳播規則本來就不對稱。一個 provider 可以把自己表裡的任何一條路由遞給 customer,這個方向暢通無阻;但反過來,一條路由要從 customer 往 provider 送、或者在兩個 peer 之間橫向送,就只能是「local」資訊,也就是自己原生、或者從自己 customer 那邊學到的路由,不能是從別的 provider 或 peer 那邊轉手來的。Cloudflare 自己把這條規則寫成一句話:「Routes propagate freely downward: a provider may hand a customer anything in its table. Propagating routes upward or sideways is restricted to 'local' information.」路由外洩,說穿了就是有人違反了這條方向規則——一條本來只該往下送的路由,被誰往上或往旁邊送了出去,流量因此被拉去一條原本不會走的路徑。

問題是,在 RFC 9234 之前,這條規則沒有協定層的表達方式。想讓路由器知道「這條路由是從 provider 那邊拿的、不能再往上送」,只能靠每個網路自己想辦法——Cloudflare 的說法是,過去每個網路都得自行把這個意圖實作出來,用的是複雜、容易出錯的路由政策;業界常見的做法包括手寫 prefix filter、AS-path 正則表達式,或者查 IRR(Internet Routing Registry)資料庫確認一個 AS 該不該出現在某條路徑裡。這套做法擋不擋得住外洩,完全看維護的人有沒有把每一條規則都寫對、寫全、更新及時。少一條規則、漏一個鄰居,外洩就直接穿過去,而且穿過去的那一刻,沒有任何協定層的訊號會告訴任何一端這裡有問題。這樣換來的保護,仍然只在寫對規則的那一段路徑上成立,往外一步就沒有任何協定層的機制接手。

RFC 9234 想做的事:把「該不該往上遞」寫進協定本身

RFC 9234 的做法,是把上一段那條方向規則直接寫進協定:BGP session 建立的時候,兩端先透過一個新的 capability 互相宣告彼此的角色——這一步就是 Cloudflare 說的「a new 'BGP Role' capability, which requires that two BGP neighbors agree on their relationship when the session comes up」。角色講好之後,路由器就能自己判斷一條路由允不允許往哪個方向送,不用再靠人工規則。真正讓下游也能受益的,是另一個新欄位:「Only to Customer」(OTC)path attribute。Cloudflare 的定義很直接:這個屬性「marks routes that must not propagate beyond customers」。依 RFC 9234 這套角色規則往下推,合理的理解是:只要一條路由是從 provider 或 peer 那邊學來的、不是 local 來源,一經過某個 AS 就會被貼上 OTC;這條路由之後不管轉幾手,只要有任何一端想把它再往上或往旁邊送,收到的那一端只要看見 OTC=1,就能直接判斷這是外洩、直接拒收,不用回頭確認鄰居是不是政策該覆蓋的清單。

這種欄位化的判斷方式,對正在寫 BGP 政策的工程師來說是實質的差異:以前要涵蓋「這個鄰居是我的 provider、那個是我的 customer」這種關係,得在每一台邊界路由器上分別維護對應的過濾規則,換一個鄰居、改一次拓樸,規則就得跟著重寫。角色宣告把這份對照表搬進協定層之後,路由器讀到 OTC 就知道該怎麼做,不用再依賴那份時常過期的規則表。

這個設計換掉的是判斷的位置:以前「這條路由能不能送」是人腦裡的一份規則表,現在變成路由本身帶著的一個位元。位元本身不會忘記更新、不會漏掉哪個鄰居——前提是協定跑對了、而且這個位元真的一路活著抵達下游。Cloudflare 自己的原文用一張圖說明這個機制的核心:「The figure below shows what B does with a route it learns from A, depending on A's relationship to B」——B 收到一條路由之後該怎麼處理,完全取決於 A 對 B 宣告的是哪一種角色,而不是 B 自己另外去查一份政策表。這正是 RFC 9234 想達成的目標:判斷邏輯不再散落在每一台邊界路由器各自維護的設定檔裡,而是收斂成「讀角色、看 OTC」這一組固定動作,兩端只要各自照協定實作,理論上就不需要再為每一個新鄰居重新寫一次規則。下面這個小工具依 RFC 9234 的規格邏輯,把 A 對 B 宣告的角色換成三種可能,看 OTC 在哪個情境下被貼上、B 又能不能把這條路由往自己的 provider 或 peer 再送出去。

切換 A 對 B 宣告的角色 · 3 種角色

A(送出路由的一端) A B(Cloudflare 一端) B OTC = 1 B 自己的 customer(一定可以送) B 自己的 provider/peer(禁止再送)

這條路由被貼上 OTC = 1

A 是 B 的 provider,這條路由不是 B 自己原生、也不是從自己 customer 學到的「local」路由,B 只能把它送給自己的 customer,不能再往自己的 provider 或 peer 送——OTC 位元把這條限制直接寫進路由本身。

照理說,這個位元應該原封不動一路傳下去

OTC 只是一個路徑屬性,跟其他 BGP 路徑屬性沒有本質上的不同——它應該就這樣附著在路由上,一路跟著轉發,沒有理由被誰單獨拿掉。這也是為什麼 Cloudflare 選擇用一種很直接的方式驗證 RFC 9234 到底跑得怎麼樣:不是去問哪些網路宣稱支援,而是實際盯著自己在全球各地跟鄰居之間的 peering session,看鄰居送過來的路由裡 OTC 到底有沒有出現。Cloudflare 自己形容這套做法是「a unique method for tracking the adoption of BGP Role configurations by monitoring which peer ASes send the OTC attribute to Cloudflare」——判斷的依據不是文件、不是宣傳稿,是實際收到的位元組。

照這個邏輯往下推,結果應該很單純:一部分鄰居已經上線 RFC 9234,送過來的路由會帶 OTC;還沒上線的鄰居,路由自然不帶。位元有沒有出現,只跟兩端有沒有各自把協定實作出來有關,中間經過幾手應該都不影響——位元不是那種需要中繼方主動維護的東西,沒有理由半路消失。

切換三個階段看保護鏈怎麼跑 · 3 個階段

RFC 9234 上線之前,擋外洩全靠 operator 自己維護的複雜、容易出錯的路由政策。這些規則擋不擋得住外洩,完全看有沒有人把每一條都寫對、寫全、更新及時——協定本身不會提醒任何一端這裡有問題。
兩端在 session 建立時先宣告彼此角色,之後只要 A 是 B 的 provider 或 peer,遞給 B 的路由就自動貼上 OTC。這條路由如果被再往上或往旁邊送,收到的一端只要看見 OTC=1,直接拒收,不用查任何一份人工維護的規則表。
Cloudflare 用自己的全球 peering 據點觀察真實流量,發現兩家大型 Tier-1 網路會把 OTC 從轉發的路由裡拿掉。位元一消失,這條路由再往下游走看起來就跟完全沒被保護過一樣——下游即使自己完全支援 RFC 9234,也看不到這個位元。

Cloudflare 真正看到的:兩家 Tier-1 把 OTC 拔掉了

Cloudflare 自己的說法是「Along the way we found something we did not expect」——這句開場已經暗示了下面要講的不是預期內的結果。他們看到的是:「two large Tier-1 networks strip the OTC attribute from routes they forward」。兩家大型 Tier-1 網路,會把這個屬性從自己轉發出去的路由裡直接拿掉。

Tier-1 這個詞在這裡指的是那種不用跟任何人買 transit、靠著互相 peering 就能到達全球所有網段的骨幹網路——合理的推測是,一條路由要從某個角落的小型 ISP 傳到另一端的小型 ISP,中間經過一家 Tier-1 並不罕見。這正是為什麼這個發現的份量比「某個小型網路沒做好」重得多:Tier-1 出現在的路徑數量本來就多,一旦它在轉發時把 OTC 拿掉,受影響的不會只是跟它直接相鄰的那一段。

這跟前一段推演的「位元不會半路消失」直接矛盾——矛盾點在於,OTC 不是靠中繼方主動維護的資訊,理論上沒有理由被誰動手拿掉,但兩家 Tier-1 確實這麼做了。文章本身沒有點名是哪兩家網路,也沒有給出任何量化數字:沒有講清楚這個現象影響了多少比例的路由、多少鄰居受到波及,只給了「兩家」這個計數,其餘只能標成未知,不能拿來腦補精確比例。

值得留意的是這個發現本身冒出來的方式:Cloudflare 沒有用「我們調查了 N 家網路,其中 M 家有問題」這種主動盤點的敘事,而是直接說「Along the way we found something we did not expect」——這件事是在追蹤 RFC 9234 整體採用率的過程中,順帶撞見的。合理的推測是,這個措辭差異透露了方法論本身的侷限:Cloudflare 用的量測方式,是被動盯著自己收到的路由裡有沒有 OTC,這種方法能發現「位元不在」,卻不必然能直接回答「位元是在哪一跳消失的」——要追出是哪兩家 Tier-1、在哪個環節動的手,得再往上一層去追查路由實際經過的路徑,而不是只看終點收到了什麼。這或許也是文章只給得出「兩家」這個計數、卻沒有給出影響比例的原因之一:被動觀察足以確認現象存在,量化影響範圍則是另一層工作。

路由方向 → 更上游 Tier-1 Peer Cloudflare OTC ✓ OTC ✕ 沒有可傳的位元 在這一跳被拿掉
示意圖,不是 Cloudflare 實際觀測到的拓樸——重點是「Tier-1」這一跳把 OTC 拿掉之後,位元往下游全部消失,Peer 與 Cloudflare 都看不到它原本存在過。

把「OTC 被拿掉」跟「OTC 本來的語意」這兩件事放在一起,可以合理推導:影響是結構性的。一旦 OTC 在某一跳被拿掉,這條路由再往下游送的時候,看起來就跟一條完全沒被 RFC 9234 保護過的路由沒有任何差別。下游網路即使自己完全支援角色宣告、也懂得看 OTC,也不會知道這條路由本來應該被標記——因為欄位已經在半路被清空了。Cloudflare 對此的回應是「We have been engaging with these Tier-1s to allow OTC attribute propagation through their networks, which aids in enabling route leak prevention capabilities for early adopters of RFC 9234」,換句話說,這件事目前還在溝通階段,不是已經解決的問題。

同一條保護鏈,三個階段各自靠什麼判斷、擋不擋得住外洩。
階段判斷主體需要什麼前提對外洩的擋法
純政策年代網路工程師人工維護複雜、容易出錯的路由政策隨時更新規則寫對才擋得住,漏一條就穿透
RFC 9234 正常運作路由器讀 OTC 位元兩端都宣告角色、正確產生 OTC看到 OTC=1 直接拒收,不查政策表
Tier-1 拔掉 OTC 之後沒有——欄位已消失無,位元在半路被清空下游看不到位元,退回等同沒有保護

這件事戳破了什麼、操作者現在能檢查什麼

這個發現戳破的,不是 RFC 9234 這個協定本身的設計——角色宣告、OTC 標記,這套邏輯在兩端之間跑起來沒有問題。戳破的是一個容易被忽略的假設:只要協定兩端都做對了,中間的每一跳就會乖乖把不認識、但也不該亂動的欄位原封不動地轉發下去。Tier-1 這種規模的網路,通常是路由經過最多跳的那幾段,一旦其中一段把 OTC 拿掉,這套保護在那之後就整條斷掉,跟兩端各自有沒有支援 RFC 9234 已經沒有關係。

往回看整個推演過程,容易被忽略的其實是一個關於「協定屬性該怎麼被中間節點對待」的預設——大部分工程師寫轉發邏輯時,遇到不認識、也不影響路由選擇的欄位,直覺是原樣轉發、不去動它。這個預設在大多數情況下成立;Cloudflare 這次觀測到的,就是這個預設在兩個具體的網路上落空的樣子。

對正在評估 RFC 9234 的網路來說,這件事給了一個具體的檢查方法:光確認自己的 session 有沒有正確宣告角色、有沒有正確產生 OTC 還不夠,還得確認送出去的那個位元,實際上有沒有活著抵達下游——去看看自己下游的鄰居收到的路由裡,OTC 是不是真的還在,而不是假設自己這端做對了、後面自然也對。具體要核對的,其實就是把 Cloudflare 這次用的方法搬到自己身上:不看設定檔寫了什麼,而是實際去看下游鄰居收到的路由裡,OTC 到底還在不在。如果自己這端明明產生了這個位元,下游卻完全看不到,問題就不在自己的設定,而在中間某一跳的轉發邏輯。合理的推測是,如果連 Cloudflare 這種規模、有能力用自己全球據點交叉比對的網路,都是先看見兩家 Tier-1 才發現這件事,規模比較小、沒有這種觀測面的網路,可能根本無從發現自己下游的保護早就斷了。

Cloudflare 這篇由 Bryton Herdes、Iliana Xygkou 與 Mingwei Zhang 三人具名發表,發布日期是 2026 年 8 月 18 日——這件事目前仍是進行式:兩家 Tier-1 是誰、拿掉 OTC 的動作是刻意的過濾規則還是實作疏漏,文章都沒有給答案;按 Cloudflare 自己的說法,截至這篇文章發布的這一天,他們仍在與這兩家 Tier-1 溝通,這件事還在溝通階段、尚未解決。對讀者來說,這代表 RFC 9234 現階段能提供的保護,邊界比協定規格書上寫的更窄一點——規格說了兩端該做什麼,卻沒有任何一份規格能保證中間經過的每一跳都會照著做、不會動手拿掉一個自己不需要的欄位。

BGP Role capability

BGP session 建立時,兩端鄰居互相宣告彼此關係的機制——Cloudflare 的說法是「a new 'BGP Role' capability, which requires that two BGP neighbors agree on their relationship when the session comes up」。

OTC(Only to Customer)

標記在路由上的一個 path attribute,作用是「marks routes that must not propagate beyond customers」——一旦設成 1,這條路由之後只能往 customer 方向送。

方向規則(provider → customer/peer ↔ peer)

Cloudflare 的說法:「Routes propagate freely downward: a provider may hand a customer anything in its table. Propagating routes upward or sideways is restricted to 'local' information.」

RFC 7908

路由外洩(route leak)的正式定義出自這份規格——一條路由的傳播範圍超出了它原本該有的邊界。

下一步該查什麼:協定兩端各自做對,不等於保護鏈路真的完整——OTC 有沒有活著送到下游,得自己去核對,不能只看自己這一端的設定。