JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

路由器內部:最長前綴比對與轉送

我們已經有了位址,也有了前綴——現在來認識那台真正在搬動封包的機器。本篇導覽打開一台路由器、把它的兩半分開,並一步步走過那條決定每個封包去向的比對規則:最長前綴比對。

一個盒子裡的兩份工作:路由與轉送

在這一層裡,我們把一個封包要在網路之間旅行所需的一切都建立起來了:帶著來源與目的位址的 IP 資料包、把位址歸成像 10.0.0.0/8 這樣一塊塊的階層式 CIDR 前綴,以及讓整套機制在全球尺度上持續運作的子網路、網路位址轉換(NAT)與 IPv6 等觀念。我們還沒做的,是打開那台真正在沿途搬動封包的機器——路由器。本篇導覽就要做這件事,並落腳在它所執行、最重要的那一條規則上。

首先要抓住的是:一台路由器其實在做兩份各自獨立的工作,而把它們搞混,是初學者最常犯的錯。這就是 轉送與路由之分。路由是緩慢、深思熟慮的規劃:路由器與鄰居們互通有無、執行演算法,並針對每一個可能的目的地區塊,算出哪個鄰居是最佳的下一步。這一切思考的產物,就是一張查找表。轉送則是快速、重複的動作:一個封包抵達,路由器瞄一眼它的目的地、查那張表,然後把封包推出正確的那道門——而它一秒鐘要做這件事好幾百萬次。

轉送表,以及為什麼一個位址會比中多列

轉送所讀的那張表,就是 轉送表。每一列都是一筆這樣形式的約定:「目的地落在這個前綴之內的封包,應該從這個介面離開、朝這個下一跳前進。」關鍵在於,各列是以 CIDR 前綴為鍵,而非以個別位址為鍵——一台路由器不可能替網際網路上數十億個 IP 位址各存一列,所以它存的是區塊的列。某一列可能寫著 10.0.0.0/8,另一列寫 10.1.0.0/16,再一列寫那條包山包海的 0.0.0.0/0。整張表,就是上一節那個路由過程凍結下來的產物。

現在來看有意思的轉折。由於各列是寬度不一的前綴,單一一個目的位址,完全可以同時合法地落在不只一列裡。回想本層稍早講過的前綴運作方式:10.1.5.7 以 10 開頭,所以它在 10.0.0.0/8 裡;它的前兩個位元組是 10.1,所以它也在 10.1.0.0/16 裡;而每一個位址理所當然都在 0.0.0.0/0 裡。那麼,哪一列勝出?它們不可能全對,因為它們可能指向不同的門。我們需要一條決勝規則,而這條決勝規則,正是整篇導覽的核心。

最長前綴比對:最具體的規則勝出

這條決勝規則就是 最長前綴比對,而它簡單得令人愉快:在所有前綴涵蓋了該位址的列當中,挑前綴最長的那一列——也就是斜線後面數字最大的那一列。前綴愈長,釘住的位址區塊就愈小、愈具體,所以最長前綴比對不過就是「最具體的指示勝出」這條規則。這跟你看路時用的常識一樣:「在銀行那裡左轉」勝過「大致往北走」,因為它對你實際所在之處說得更精準。

Forwarding table (destination prefix -> outgoing interface)

  prefix            interface     prefix length
  10.0.0.0/8        eth1          8
  10.1.0.0/16       eth2          16
  10.1.5.0/24       eth3          24
  0.0.0.0/0         eth0          0   (the default route)

Packet arrives for destination 10.1.5.7

  10.1.5.7 in 10.0.0.0/8 ?    yes   (matches 8 bits)
  10.1.5.7 in 10.1.0.0/16?    yes   (matches 16 bits)
  10.1.5.7 in 10.1.5.0/24?    yes   (matches 24 bits)  <-- longest
  10.1.5.7 in 0.0.0.0/0  ?    yes   (matches 0 bits)

  Winner: 10.1.5.0/24  ->  send out eth3
三個前綴都比中了 10.1.5.7,但 /24 最具體,所以封包從 eth3 離開。

看看最後那一列,0.0.0.0/0——一個長度為零的前綴比中絕對每一個位址,因為它什麼都沒釘住。這就是 預設路由,也就是「若沒有更具體的列比中,就往這個方向送」的後備。這正是你家裡路由器所用的規則:它精準地認得那寥寥幾個本地位址,而對於整個網際網路上的其他一切,它就聳聳肩、朝你的網際網路服務供應商轉送過去。因為它的前綴長度是 0,最長前綴比對只會在不存在更好、更具體的列時,才回退到它——而這正是我們想要的行為。

查找實際上如何運行,一跳接一跳

讓我們慢慢地把一個封包穿過一台路由器追蹤一遍,好讓機制變得具體。要記住的關鍵是:這整支舞,會在路徑上每一台路由器各自獨立地上演一次——沒有任何一台路由器知道或在乎完整的路線,它只回答「給定這個目的地,我哪一道門是下一步?」這一個問題。封包本身不帶路線;它只帶著自己的目的位址,而每一台路由器都從頭重新決定一次。正是這種一跳接一跳的獨立性,讓網際網路的資料包模型如此強韌。

  1. 一個封包抵達某個輸入埠。路由器從封包標頭裡讀出目的 IP 位址——就轉送而言,它無視其餘一切,包括來源位址與酬載。
  2. 它執行最長前綴比對:在所有前綴涵蓋了該目的地的轉送表列當中,挑出前綴最長的那一列。那一列指明了一個外出介面與一個下一跳。
  3. 它把封包的存活時間(TTL)欄位減一。若 TTL 歸零,代表這個封包繞圈或漂泊太久了,於是路由器丟棄它、並回送一則 ICMP「逾時」訊息——回想稍早所學,這正是 traceroute 窺探每一跳的方式。
  4. 它把封包交給交換結構,也就是把封包從輸入埠運過去、送到選定輸出埠的那條內部高速公路——理想上,不會與同一瞬間正被調度的其他數百萬個封包相撞。
  5. 輸出埠把封包排入佇列,然後將它發送到下一條連結上。在另一端,下一台路由器又從第一步開始——重新查目的地,對封包去過哪裡毫無記憶。

為什麼用前綴——以及幾點誠實的提醒

退一步,注意這些零件契合得多麼漂亮。位址是階層式的(第二篇導覽),所以相鄰的位址共用一個前綴;CIDR 讓我們用一個前綴就能命名任意大小的區塊;而最長前綴比對,讓一台路由器能用一條極小的通則涵蓋一整片廣大區域,同時還容許底下的特殊子區塊有精準的例外。這就是 路由聚合的實際運作——單單一列 /8,就能代表一千六百萬個位址,這也是網際網路的路由表竟然塞得進一台路由器的唯一原因。位址中的階層讓表格保持小巧;最長前綴比對則讓這些小表格在要緊之處仍能精確無誤。

幾點誠實的提醒,免得你過度信任這幅圖像。第一,最長前綴比對只決定下一跳——它不是一條路線,而是一個步驟,會在每一跳被重新決定一次,這也是為什麼一組設定錯誤的路由器,可能把封包送著繞圈、直到它的 TTL 過期。第二,轉送表的好壞,完全取決於填它的那套路由;若路由餵給它一個錯誤或被劫持的前綴,轉送會忠實地把封包往錯的方向送,因為它從不質疑那張表。第三,目的位址命名的是一個介面,而非一個人、也非一台永久的裝置——同一套查找機制把封包朝網路上的某個位置移動,卻完全不知道那裡實際上是誰、是什麼。

至此,網路層的資料平面就完整了。你現在能描述整段旅程:一個封包帶著 IP 標頭被組建起來(第一篇導覽)、用切成 CIDR 前綴的 IPv4 或 IPv6 來定址(第二與第四篇導覽)、在外出途中或許經過 NAT 轉換(第三篇導覽),接著由各自對著一張表執行最長前綴比對的路由器一跳接一跳地轉送——就是本篇導覽。我們刻意留作黑盒子的那一塊,是這些表一開始究竟是如何被填起來的。那就是路由,也是下一層整層的主題,在那裡,路由器終於學會彼此交談、建起那張地圖。