全球 IPv4 地址耗盡,IPv6 來了!
- 2019 年 12 月 3 日
- 筆記

本文作者:robintang,騰訊 WXG 後台開發工程師。轉載自「 雲加社區」。
就在昨天,2019 年 11 月 26 日,全球 43 億個 IPv4 地址正式耗盡,很多人表示憂慮。不過不用擔心,IPv4 的下一代 IP 協議 IPv6 將會從根本上解決 IPv4 地址耗盡的問題。
下面通過一篇長文來了解下什麼是 IPv6。
主要內容包括:
- IPv6 的基本概念
- IPv6 在 Linux 作業系統下的實現
- IPv6 的實驗
- IPv6 的過渡技術介紹
- IPv6 在 Linux 平台下 socket 編程應該注意的問題
- 實現簡易版 TGW 支援 IPv6 雛形 demo
IPv6 的基本概念
2017 年 11 月 26 日,中共中央辦公廳和國務院辦公廳印發了《推薦互聯網協議第六版(IPv6)規模部署行動計劃》,並發出通知,要求各地區各部門結合實際認真貫徹落實。這條新聞傳達了一個很重要的資訊:這個是推進中國 IPv6 發展的戰略總動員令。
值得說的是,目前我們接觸得比較多的主流作業系統內核,已經很好地支援 IPv6 協議棧,例如:
- Windows:windows 7、windows 8.x、windows 10,默認開啟 IPv6
- Linux:內核 2.6.x、內核 3.x、內核 4.x 已經支援 IPv6(需要手動開啟)
IOS:IOS9 開始已經支援 IPv6 Only,2016 年蘋果已經強制要求 app 必須支援 IPv6
本文提到的 IPv6 節點,沒有特殊說明,一般指的是純 IPv6 節點(IPv6 Only),也就是只支援 IPv6 協議棧;IPv4 節點,是指純 IPv4 的節點,也就是只支援 IPv4 協議棧;如果節點支援 IPv6 和 IPv4 雙棧,會指明是雙棧節點。
IPv6 採用 128 位的地址長度擁有更大的地址空間,首先我們先來認識一下 IPv6 到底長成什麼樣子。
初識 IPv6

圖1 IPv6數據報文
上圖是我們最熟悉的 ping 的 IPv6 版本 ICMPv6。可以看到,IPv6 數據報文和 IPv4 有很大的差別:
- 數據鏈路層(L2)的 type 欄位標識為 0x86dd,表示承載的上層協議是 IPv6
IPv4 對比:type 欄位為 0x0800
- IPv6 的頭部欄位,和 IPv4 差別巨大(可以猜測到,IPv6 和 IPv4 無法兼容)
IPv6 的報文頭部格式如下:

圖2 IPv6報文頭部(該圖片來自互聯網)
IPv6 報文頭部更精簡了,欄位更少了,對比起 IPv4,有以下幾個地方值得注意:
- IPv6 報文頭部是定長(固定為 40 位元組),IPv4 報文頭部是變長的。這個意味著,寫程式碼處理 IPv6 數據報文的效率會提高很多:)
- IPv6 中 Hop Limit 欄位含義類似 IPv4 的 TTL。
- IPv6 中的 Traffic Class 欄位含義類似 IPv4 中的 TOS(Type Of Service)。
- IPv6 的報文頭部取消了校驗和欄位。取消這個欄位也是對 IPv4 協議的一個改進。當 IPv4 報文在網路間傳輸,每經過一個路由器轉發就是修改 TTL 欄位,就需要重新計算校驗和,而由於數據鏈路層 L2 和傳輸層 L4 的校驗已經足夠強壯,因此 IPv6 取消這個欄位會提高路由器的轉發效率。值得一提的是,在 IPv6 協議下,傳輸層 L4 協議 UDP、TCP 是強制需要進行校驗和的(IPv4 是可選的)。
- IPv6 報文頭部中的 Next Header 欄位表示「承載上一層的協議類型」或者「擴展頭部類型」。這裡的含義與 IPv4 有很大的差別,需要加以解釋:
當 IPv6 數據報文承載的是上層協議 ICMPv6、TCP、UDP 等的時候,Next Header 的值分別為 58、6、17,這個時候和 IPv4 報文頭部中的 Protocol 欄位很類似。
當不是以上 3 種協議類型的時候,IPv6 報文頭部緊接的是擴展頭部。擴展頭部是 IPv6 引入的一個新的概念,每個 IPv6 的數據報文可以承載 0 個或多個擴展頭部,擴展頭部通過鏈表的形式組織起來。當 IPv6 數據報文承載著擴展頭部的時候,Next Header 的數值為擴展頭部的類型值。
為什麼要引入擴展頭部這個概念,這裡也是 IPv6 對 IPv4 改進的一個方面,用擴展頭部取代了 IPv4 的可選項資訊,精簡了 IPv6 的頭部,增強了 IPv6 的擴展性。有同學會不會有疑問,IPv6 的分片數據報文怎麼處理?其實就是使用了 IPv6 擴展頭部。我們來抓一個 UDP 分片報文來看看。

圖3 IPv6分片報文
當發送一個分片 IPv6 數據報文的時候,IPv6 使用的是擴展頭部的形式組織各個分片的資訊,如圖 IPv6 報文頭部 Next Header 欄位值為 44 表示存在擴展頭部,擴展頭部是 IPv6 分片數據資訊。
對比 IPv4,分片資訊是記錄在 IPv4 報文頭部的分片欄位中。
IPv6 的擴展頭部類型有很多種,除了上述的分片頭部,還有路由頭部、逐跳可選頭部等,具體的可以參考 RFC2460。
本章主要介紹了 IPv6 的一些很直觀的認識,下面逐漸介紹 IPv6 上的基本知識和概念。
IPv6 的地址語法
一個 IPv6 的地址使用冒號十六進位表示方法:128 位的地址每 16 位分成一段,每個 16 位的段用十六進位表示並用冒號分隔開,例如:
一個普通公網 IPv6 地址:2001:0D12:0000:0000:02AA:0987:FE29:9871
IPv6 地址支援壓縮前導零的表示方法,例如上面的地址可以壓縮表示為:
2001:D12:0:0:2AA:987:FE29:9871
為了進一步精簡 IPv6 地址,當冒號十六進位格式中出現連續幾段數值 0 的位段時,這些段可以壓縮為雙冒號的表示,例如上面的地址還可以進一步精簡表示為:
2001:D12::2AA:987:FE29:9871
又例如 IPv6 的地址 FF80:0:0:0:FF:3BA:891:67C2 可以進一步精簡表示為:
FE80::FF:3BA:891:67C2
這裡值得注意的是,雙冒號只能出現一次。
IPv6 地址的號段劃分和前綴表示法
IPv6 擁有 128 位巨大的地址空間,對於那麼大的空間,也不是隨意的劃分,而是使用按照 bit 位進行號段劃分(與鵝廠內部一些的 64 位 uin 改造放號的 zone 劃分演算法)。
IPv6 的地址結構如下圖:

圖4 IPv6地址結構
例如 RFC4291 中定義了 n=48, m=16,也就是子網和介面 ID 與各占 64 位
IPv6 支援子網前綴標識方法,類似於 IPv4 的無分類域間路由 CIDR 機制(注意:IPv6 沒有子網掩碼 mask 的概念)。使用「IPv6 地址/前綴長度」表示方法,例如:
2001:C3:0:2C6A::/64 表示一個子網
而 2001:C3:0:2C6A:C9B4:FF12:48BC:1A22/64 表示該子網下的一個節點地址。
可以看到,一個 IPv6 的地址有子網前綴+介面 ID 構成,子網前綴由地址分配和管理機構定義和分配,而介面 ID 可以由各作業系統實現生成,生成演算法後面的章節會介紹。
IPv6 的地址類型
IPv6 地址分三種類型
1、單播,對應於 IPv4 的普通公網和私網地址
2、組播,對應於 IPv4 的組播(多播)地址
3、任播,IPv6 新增的地址概念類型
IPv6 沒有廣播地址,用組播地址實現廣播的功能。實際上我們工作和生活最可能最多接觸的就是單播地址,接下來本文重點會講解單播地址的種類。組播和任播地址有興趣的同學自行查閱相關 RFC 和文獻。
IPv6 單播地址
注意,大家如果在網上搜索 IPv6 的地址,可能都是千篇一律的把所有「出現過」的單播地址介紹出來,其實有一些單播地址類型已經在相關的 RFC 中被廢除或者不建議使用,而本節會指出這類地址。同時,在介紹單播地址的時候,盡量與 IPv4 中對應的或者相類似的概念做對比,加深理解。
IPv6 單播地址有以下幾種:
1、全球單播地址

圖5 IPv6全球單播地址結構
前綴 2000::/3,相當於 IPv4 的公網地址(IPv6 的誕生根本上就是為了解決 IPv4 公網地址耗盡的問題)。這種地址在全球的路由器間可以路由。
2、鏈路本地地址

圖6 鏈路本地地址結構
前綴 FE80::/10,顧名思義,此類地址用於同一鏈路上的節點間的通訊,主要用於自動配置地址和鄰居節點發現過程。Windows 和 Linux 支援或開啟 IPv6 後,默認會給網卡介面自動配置一個鏈路本地地址。也就是說,一個介面一定有一個鏈路本地地址。如下圖:

圖7 Linux下查看鏈路本地地址

圖8 Windows下查看鏈路本地地址
值得說的是,每個介面必須至少有一個鏈路本地地址;每個介面可以配置 1 個以上的單播地址,例如一個介面可以配置一個鏈路本地地址,同時也可以配置一個全球單播地址。
注意,很容易會把鏈路本地地址和 IPv4 的私網/內網地址對應起來,其實鏈路本地地址對應於 IPv4 的 APIPA 地址,也就是 169.254 開頭的地址(典型場景就是 windows 開啟自動獲取地址而獲取失敗後自動分配一個 169.254 的地址)。而 IPv4 私網對應於 IPv6 的什麼地址,後面會介紹。
特別地,在 IPv6 socket 編程中,可以使用鏈路本地地址編程通訊,但是需要增加一些額外的參數(這是一個小坑),在後面介紹編程的章節會介紹。
3、唯一本地地址

圖9 唯一本地地址結構
前綴 FC00::/7,相當於 IPv4 的私網地址(10.0.0.0、172.16.0.0、192.168.0.0),在 RFC4193 中新定義的一種解決私網需求的單播地址類型,用來代替廢棄使用的站點本地地址。
可能看到這裡,有同學會跳出來說:IPv6 不是為了解決 IPv4 地址耗盡的問題嗎,既然 IPv6 的地址空間那麼大,可以為每一個網路節點分配公網 IPv6 的節點,那為什麼 IPv6 還需要支援私網?這裡需要談談對 IPv6 下私網支援的認識。
在 IPv4 中,利用 NAT 技術私網內的網路節點可以使用統一的公網出口訪問互聯網資源,大大節省了 IPv4 公網地址的消耗(IPv6 推進緩慢的原因之一)。另一方面,由於默認情況下私網內節點與外界通訊的發起是單向的,網路訪問僅僅能從私網內發起,外部發起的請求會被統一網關或者防火牆阻隔掉,這樣的網路架構很好的保護了私網內的節點安全性和私密性。可以設想一下,如果鵝廠內部每台辦公電腦都配置了 IPv6 的公網地址上網,是多麼可怕的事情,每台辦公電腦都會面臨被黑客入侵的威脅(肉雞真多)。
因此,在安全性和私密性要求下,IPv6 中同樣需要支援私網,並且也需要支援 NAT。在 Linux 內核 3.7 版本開始加入對 IPv6 NAT 的支援,實現的方式和 IPv4 下的差別不大(Linux 內核程式碼中變數和函數的命名幾乎就是 ctrl+c 和 ctrl+v 過來的-_-||)。
4、站點本地地址
前綴 FEC9::/48,以前是用來部署私網的,但 RFC3879 中已經不建議使用這類地址,建議使用唯一本地地址。大家知道有這麼一回事就可以了。網上還有很多文章還提到這種地址,但是沒有說明這種地址已經不再使用。
5、特殊地址:迴環地址
0:0:0:0:0:0:0:1 或::1,等同於 IPv4 的 127.0.0.1
6、過渡地址:內嵌 IPv4 地址的 IPv6 地址
就是在 IPv6 的某一些十六進位段內嵌這 IPv4 的地址,例如 IPv6 地址中 64:ff9b::10.10.10.10,此 IPv6 地址最後 4 個位元組內嵌一個 IPv4 的地址,這類地址主要用於 IPv6/IPv4 的過渡技術中。
一、IPv4 兼容地址
0:0:0:0:0:0:w.x.y.z 或::w.x.y.z(其中 w.x.y.z 是點分十進位的 IPv4 地址)。但在 RFC4291 中已經不推薦使用這類地址,大家知道有這麼一回事就可以了。
二、過渡地址:IPv4 映射地址
0:0:0:0:0:FFFF:w.x.y.z 或::FFFF:w.x.y.z(其中 w.x.y.z 是點分十進位的 IPv4 地址),用於 IPv6 地址表示 IPv4 地址。主要用於某些場景下 IPv6 節點與 IPv4 節點通訊,Linux 內核對這類地址很好地支援,在後面編程和內核分析的章節會分析使用過程。
三、過渡地址:特定過渡技術地址
6to4 地址、ISATAP 地址、Teredo 地址主要用於對應的過渡技術的地址,在後面介紹過渡技術的時候會介紹。
IPv6 介面 ID 生成演算法
從前面的介紹中可以看出,IPv6 單播地址是由前綴(64 位)+介面 ID(64 位)組成。介面 ID 的生成演算法主要有以下幾種:
1、根據 RFC4291 定義,介面 ID 可以從 EUI-64 地址生成。
詳細演算法可以查看 regli 同學的 PPT 第 14 頁。
2、為了可以具備某種程度的匿名信,介面 ID 可以使用一個隨機分配的,windows 作業系統默認就是使用這種生成演算法,Linux 下也是默認開啟這個演算法。
3、使用狀態化的自動配置技術分配,例如 DHCPv6 分配。
4、手工配置。
IPv6 地址配置
前面對 IPv6 的地址、前綴、介面等等做了介紹,接下來就是要介紹一個介面如何配置 IPv6 地址。IPv6 一個比 IPv4 更厲害的方面,就是可以自動配置地址,甚至這個配置過程不需要 DHCPv6(在 IPv4 中是 DHCPv4)這樣的地址配置協議。最典型的例子就是,只要開啟了 IPv6 協議棧的作業系統,每個介面就能自動配置了鏈路本地地址,這個是和 IPv4 最重要的區別之一。
IPv6 的地址配置有以下幾種:
1、只要開啟了 IPv6 協議棧,介面自動分配鏈路本地地址。
2、無狀態自動配置地址(RFC2462),後面會有實驗演示。
3、有狀態自動配置地址,例如 DHCPv6。
4、手動配置。
IPv6 的域名解析
由於 IPv6 的地址擴展為 128 位,比 IPv4 的更難書寫和記憶,因此 IPv6 下的 DNS 變得尤為重要。IPv6 的的 DNS 資源記錄類型為 AAAA(又稱作 4A),用於解析指向 IPv6 地址的完全有效域名。下面是一個示例:
Hostipv6.example.wechat.com IN AAAA 2001:db8:1::1
IPv6 下的域名解析可以認為是 IPv4 的擴展,詳細可以查看 RFC3596。
Linux 內核 IPv6 架構簡析
本文後面主要的分析都是基於 Linux,會有涉及關於 Linux 內核對 IPv6 的實現。主要是因為,現在 IPv6 的參考資料不多,除了與 IPv6 相關的 RFC 之外,還有少數可以參閱的 IPv6 國外文獻,而 Linux 內核一直都與跟隨著 IPv6 的協議更新和變化,Linux 內核 IPv6 的實現是十分重要的參考材料之一。而且從事後台開發工作主要也是在 Linux 平台下,熟悉 Linux 下 IPv6 的實現也是為以後的工作做知識儲備。
PS:客戶端開發的同學可以參考各自平台的文檔………….
Linux 在很早之前就已經開始支援 IPv6,目前我們接觸最多的 Linux 內核版本都很好地支援 IPv6,同時也是支援 IPv4/IPv6 雙棧體系。在 Linux 作業系統中,IPv4 是默認必須開啟,IPv6 是可選編譯和配置開啟。
例如在編譯內核的時候,需要選擇 IPv6 編譯選項才支援 IPv6

圖10 Linux內核編譯支援IPv6
當開啟支援 IPv6 的 Linux 的內核網路雙棧的結構,如下圖:

圖11 Linux內核雙棧架構
Linux 內核中,IPv6 協議棧與 IPv4 協議棧並行關係。IPv6 和 IPv4 完全是兩套不一樣的程式碼實現。IPv6 完整的協議棧邏輯模組包括:
1、網路層 IPv6,核心邏輯:IPv6 路由子系統
2、傳輸層 TCP/UDP 實現:TCPv6、UDPv6
3、控制報文協議 ICMPv6,這裡值得一提的是 ICMPv6 在 IPv6 協議中的地位十分重要。
ICMPv6 不僅提供了與 ICMPv4 相同的服務診斷功能,例如報告數據包的錯誤和提供簡單的 echo 服務,ICMPv6 是 IPv6 中鄰居發現協議的重要組成部分,用於管理鏈路上的點到點的通訊。
4、鄰居子系統的實現:鄰居發現協議 NDP(對應於 IPv4 裡面的 ARP 協議)
5、其他高級實現(IPv6 NAT、IPv6 隧道、iPv6 IPSec 等)
由於我們平時的開發工作在應用層,以上 1-4 是將會接觸得最多。
IPv6 實驗
本章我們通過實驗,加深對 IPv6 的認識。這裡的實驗沒有使用真實現網的 IPv6 接入點(目前中國絕大部分接入點都是教育網),而實驗的目的主要是觀察 IPv6 的數據包結構、IPv6 的路由配置等,所以決定自己通過搭建中間路由器、應用伺服器的方式做實驗,便於抓包和程式碼分析。
客戶端:windows 7
路由器:中間路由器使用自己編譯和搭建的 Linux 系統(內核 2.6.32.27)
應用伺服器:Ubuntu16.04LTS 版本。
為什麼要使用自己編譯的 Linux 作為路由器?因為 IPv6 的實踐類能參考的文獻比較少,而 Linux 內核的 IPv6 模組是最重要的參考資源之一,在實踐中遇到問題可以使用打 LOG 和分析程式碼的方法解決。
1、無狀態自動配置地址實驗
IPv6 地址的獲取是最重要的環節之一。本實驗使用開源的無狀態自動配置服務 radvd 進行實驗。

圖12 IPv6無狀態自動配置

圖13 IPv6無狀態自動配置報文分析
無狀態自動配置過程:
1、由鏈路上的主機向鏈路發起「路由請求」報文,這個報文是以組播協議發送,尋找鏈路上最合適的路由器。
2、路由器收到請求會返回「路由通告」報文,報文裡面帶著本鏈路的地址前綴資訊主機將接收到的前綴和自身的介面 ID,組成完整的新地址。
3、主機嘗試使用新地址發起地址重複檢測,檢測鏈路上是否有其他主機也是這個地址,如果有,就停止使用該地址;如果沒有,就啟用這個新地址。
可以看到無狀態自動配置過程十分簡易(對比 DHCPv4 和 DHCPv6 來說),實際上,無狀態自動配置可以單獨組網使用,也可以配合有狀態自動配置一般會配合使用,加強網路節點管理。涉及自動配置和地址檢測等更多細節,可以查閱 RFC1971、RFC4861。
2、IPv6 靜態路由配置實驗
本次實驗主要是了解 windows 和 linux 的靜態路由配置。

圖14 IPv6典型的網路拓撲
由於各自的網路前綴(網段)不一致,在不使用默認路由的情況下,我們嘗試配置路由讓客戶端可以訪問到伺服器。
一、Windows 7 配置靜態路由:
去往伺服器的 2001:db8:5::/64 網段的路由

圖15 Windows配置IPv6路由
二、路由器 1 配置

圖16 Linux下配置IPv6路由
三、路由器 2 配置

圖17 Linux下配置IPv6路由
四、伺服器靜態路由配置

圖18 伺服器配置IPv6路由
五、結果

圖19 客戶端訪問伺服器
客戶端可以順利 ping 通伺服器。可以看到,IPv6 下的路由配置,無論是 windows 還是 linux,與 IPv4 的配置差別不大,熟悉 IPv4 各個平台路由配置的同學可以很快上手 IPv6 的路由配置。
3、IPv6 的 web 服務
復用 2 的架構,在伺服器端部署一個 web 服務,在客戶端訪問該 web 服務。web 服務沒有選擇像 apache 或者 nginx 這樣的龐然大物,而選擇了很輕量的 boa。原因是 boa 雖然原始支援 IPv6,但是我想粗暴的把所有 IPv4 的 socket 套接字都替換成 IPv6 版本,嘗試做一個自定義的升級。結果需要改動的程式碼非常少,不超過 20 行,boa 就能完全支援 IPv6。
配合實驗,寫了一個簡單的 CGI,只是在版面 echo 字元串。如下圖:

圖20 瀏覽器使用IPv6地址訪問網路資源
這裡值得注意的是,在瀏覽器中使用 IPv6 的地址訪問 web 資源,IPv6 的地址必須要使用中括弧「[]」包起來。

圖21 IPv6下的http報文
從 Server 端抓包看,IPv6 下的 Web 服務 http 報文,除了網路層 L3 的報文頭部不一樣之外,其餘的都和 IPv4 版本的沒有太大差別差別。
4、IPv6 的過渡技術實驗
這部分將在過渡技術介紹中一起實驗。
IPv6 的過渡技術
IPv6 的提出,最重要的目的就是解決公網 IPv4 耗盡的問題,而且 IPv6 協議的設計就考慮到了更加好的效率、安全、擴展等方面,可以那麼說,IPv6 是未來網路發展的大趨勢。但為什麼 IPv6 已經發展了十幾年了,目前在我們的工作和生活中還是比較少接觸和使用。這裡的原因是非常的複雜,有技術上障礙,因為 IPv6 和 IPv4 是兩個完全不兼容的協議(在極少數的特定場景可以實現兼容),如果要從支援 IPv4 升級到 IPv6,無論是應用程式用客戶端、伺服器程式端、路由器等等,都要同時支援 IPv6 才能解決問題,這個的升級改造需要花費的成本是巨大的。而且,正是由於技術上的升級花費大量的人力物力,無論是運營商還是互聯網服務商,一方面要重視用戶的體驗問題,這個肯定不能強制客戶更新換代硬體設備和軟體,另一方面也要維護自身的投資和利益,更願意去選擇利用現有技術降低 IPv4 地址耗盡帶來的壓力,例如 NAT 的廣泛應用,就是 IPv6 推廣使用的一個重要的「障礙」。
由上所述,IPv4 升級到 IPv6 肯定不會是一蹴而就的,是需要經歷一個十分漫長的過渡階段(用我廠通用的術語說,就是 IPv4 升級 IPv6 這個灰度的時間非常長),要數十年的時間都不為過。現階段,就出現了 IPv4 慢慢過渡到 IPv6 的技術(或者叫過渡時期的技術)。過渡技術要解決最重要的問題就是,如何利用現在大規模的 IPv4 網路進行 IPv6 的通訊。
要解決上面的問題,這裡主要介紹 3 種過渡技術:
1、雙棧技術
2、隧道技術
3、轉換技術(有一些文獻叫做翻譯技術)
本章節會對以上的過渡技術,選取幾個典型的、我們未來最有機會接觸到的具體的過渡技術結合實驗觀察過渡技術的具體實現和數據包的表現形式。
雙棧技術
這種技術其實很好理解,就是通訊節點同時支援 IPv4 和 IPv6 雙棧。例如在同一個交換機下面有 2 個 Linux 的節點,2 個節點都是 IPv4/IPv6 雙棧,節點間原來使用 IPv4 上的 UDP 協議通訊傳輸,現在需要升級為 IPv6 上的 UDP 傳輸。由於 2 個節點都支援 IPv6,那隻要修改應用程式為 IPv6 的 socket 通訊基本達到目的了。
上面的例子在區域網通訊的改造是很容易的。但是在廣域網,問題就變得十分複雜了。因為主要問題是在廣域網上的 2 個節點間往往經過多個路由器,按照雙棧技術的部署要求,之間的所有節點都要支援 IPv4/IPv6 雙棧,並且都要配置了 IPv4 的公網 IP 才能正常工作,這裡就無法解決 IPv4 公網地址匱乏的問題。因此,雙棧技術一般不會直接部署到網路中,而是配合其他過渡技術一起使用,例如在隧道技術中,在隧道的邊界路由器就是雙棧的,其他參與通訊的節點不要求是雙棧的。
隧道技術
當前的網路是以 IPv4 為主,因此儘可能地充分利用 IPv4 網路進行 IPv6 通訊是十分好的手段之一。隧道技術就是這樣子的一種過渡技術。
隧道將 IPv6 的數據報文封裝在 IPv4 的報文頭部後面(IPv6 的數據報文是 IPv4 的載荷部分),IPv6 通訊節點之間傳輸的 IPv6 數據包就可以穿越 IPv4 網路進行傳輸。隧道技術的一個很重要的優點是透明性,通過隧道進行通訊的兩個 IPv6 節點(或者節點上的應用程式)幾乎感覺不到隧道的存在。

圖22 IPv6典型的隧道
上圖是一種典型的隧道技術:路由器-路由器隧道,兩個 IPv6 網路中的主機通過隧道方式穿越了 IPv4 進行通訊。其中 C 節點和 D 節點被稱為邊界路由器,邊界路由器必須要支援 IPv4-IPv6 雙棧。當 IPv6 網路 1 的主機 A 將 IPv6 數據包發給邊界路由器 C,C 對 IPv6 數據包進行 IPv4 封裝,然後在 IPv4 網路上進行傳輸,發送到邊界路由器 D,D 收到 IPv4 的數據包後剝掉 IPv4 的包頭,還原 IPv6 的數據包,發送到 IPv6 網路 2 的主機 B。
根據隧道的出口入口的構成,隧道可以分為路由器-路由器,主機-路由器隧道、路由器-主機、主機-主機隧道等類型。
隧道的類型也分為手動配置類型和自動配置類型兩種,手動配置是指點對點的隧道是手動加以配置,例如手動配置點對點隧道外層的 IPv4 地址才能建立起隧道;自動配置是指隧道的建立和卸載是動態的,一般會把隧道外層的 IPv4 地址內嵌到數據包的目的 IPv6 地址裡面,在隧道路由器獲取該 IPv6 地址時候取出內嵌 IPv4 地址從而使用該 IPv4 地址作為隧道的對端來建立隧道。
下面就介紹幾種我們很可能會接觸到的具體的隧道技術。
在介紹具體的隧道技術前,特別要說明一下,Linux 內核原生支援一種叫做 sit(Simple Internet Transition)隧道。這個隧道專門用於 IPv6-in-IPv4 的數據封裝解封和傳輸,應用十分之廣泛,現在很多主流的 IPv6 隧道技術都能基於 sit 隧道實現。關於 sit 隧道的技術實現,可以查閱 Linux 內核源碼 net/ipv6/sit.c 。
1、6to4 隧道
6to4 是當前使用得比較廣泛的一種自動配置隧道技術,這種技術採用特殊的 IPv6 地址,稱為 6to4 地址,這種地址是以 2002 開頭,接著後面的 32 位就是內嵌的隧道對端的 IPv4 地址。當邊界路由器收到這類目的地址,取出 IPv4 地址建立隧道。
6to4 隧道一般用在路由器-路由器、主機-路由器、路由器-主機場景,典型的應用場景是兩個 IPv6 的站點內主機通過 6to4 隧道進行相互訪問。
6to4 隧道的一個限制是內嵌的 IPv4 地址必須是公網地址。
6to4 隧道實驗
如下圖,就是本次 6to4 實驗中使用的隧道架構,該架構是典型的路由器-路由器隧道,隧道兩側的 IPv6 網路對隧道的存在無感知。

圖23 6to4路由器-路由器隧道
在 Linux 下的 sit 隧道可以自適應為 6to4 隧道。

圖24 Linux下配置sit隧道(6to4)
上圖就是在路由器上配置 sit 隧道的命令,因為是使用 6to4 隧道,隧道的目的端點地址是從目的地址中獲取,因此只需要配置本地端點即可。

圖25 瀏覽器通過隧道訪問web服務
配置完隧道後,使用客戶端訪問 web 服務,可以正常訪問。

圖26 web伺服器端抓取http報文
在 web 服務端抓取 http 報文,可以看到,web 服務獲取到就是一個普通的 http 請問報文。

圖27 隧道內抓取http報文
在隧道內抓取 http 報文,可以看到裡面的乾坤。這個不是一般的 http 報文,它比服務端抓取到的多了一層 IPv4 報文頭部,是隧道的外出通訊協議,隧道內層 IPv6 才是真正的數據。IPv4 報文頭部中的協議欄位,不是我們熟悉的 TCP(6)/UDP(17)協議,而是 IPv6-in-IPv4 專屬的隧道協議類型。
可以看到,經過隧道的數據報文,在隧道兩端的邊界路由器分別完成了隧道協議的封包和解包,在真正獲取到數據的節點看來,幾乎不感知隧道的存在。
2、ISATAP 隧道
ISATAP 全稱是站點內自動隧道定址協議(Intra-Site Automatic Tunnel Addressing Protocol),用來為 IPv4 網路中的 IPv6 雙棧節點可以跨越 IPv4 網路訪問外部的 IPv6 節點。
ISATAP 隧道一般用於主機-主機、主機-路由器的場景。
ISATAP 隧道實驗
如下圖就是本次實驗使用的架構,是一種典型的主機-路由器場景。實驗中需要在路由器 2 上部署 radvd 服務,用於客戶端進行無狀態自動配置地址。Linux 下的 ISATAP 隧道也是可以使用 sit 隧道實現。

圖28 ISATAP主機-路由器隧道

圖29 Windows下配置ISATAP隧道
實驗用的客戶端使用 windows 7,原生支援 ISATAP 隧道,如上圖,需要進入 netsh 開啟並且設置 ISATAP 的路由器地址(支援域名)。

圖30 ISATAP隧道中的無狀態自動配置
當客戶端設置完 router 後,隧道已經建立,客戶端便發起了無狀態自動配置流程,可以看到上面的截圖路由器通過隧道將前綴資訊下發給客戶端,客戶端完成無狀態自動配置,獲取到公網 IP 地址。

圖31 ISATAP隧道介面地址
在 windows 7 上查看 ISATAP 介面,獲取到公網地址。這個地址類型是 ISATAP 專用的地址結構,由 64 位全球單播路由前綴:200(0):5e5f:w.x.y.z 組成(w.x.y.z 是客戶端的 IPv4 地址)。

圖32 使用ISATAP隧道訪問web服務
如上圖,使用 ISATAP 隧道訪問 web 服務,在隧道內的數據抓包,可以看到和 6to4 的類似,這裡就不再深入闡述。
3、Teredo 隧道
前面的隧道技術,主要是在 IPv4 的數據報文承載著 IPv6 的數據報文,這是一種特殊的數據包格式(IPV6-in-IPv4),不同於我們熟悉的 TCP、UDP 等傳輸層協議。而我們平常接觸到的網路都存在於 NAT 架構中(例如我們的辦公網路和家庭網路),在這種網路架構中,路由器僅對於 TCP、UDP 等傳輸層協議做 NAT 處理,而無法正確處理 IPv6-in-IPv4 這種報文,例如使用 ISATAP 隧道,IPv6 雙棧節點與 ISATAP 路由器之前如果存在 NAT,ISATAP 建立隧道失敗;6to4 隧道也會遇到同樣的問題。
Teredo 隧道是有微軟公司主導的一項隧道技術,主要用於在 NAT 網路架構下建立穿越 NAT 的隧道。
Teredo 隧道的核心思路,是將 IPv6 的數據封裝成 IPv4 的 UDP 數據包,利用 NAT 對 IPv4 的 UDP 支援進行穿越 NAT 的傳輸,當 UDP 包到達隧道的另外一端後,再把 IPv4 的包頭、UDP 包頭剝離,還原 IPv6 的數據包,再進行下一步的 IPv6 數據通訊轉發。Teredo 節點會分配一個以 2001::/32 的前綴,而且地址中還包含 Teredo 的伺服器、標誌位和客戶端外部映射模糊地址和埠號等資訊。
Teredo 的實現還會遇到 NAT 的類型不同而被限制的問題。NAT 的類型有錐形 NAT、受限制的 NAT、對稱 NAT 幾種,Teredo 只能在錐形 NAT 和受限制的 NAT 的環境下正常工作,而且在這兩種 NAT 需要處理的邏輯又是不一樣的。因此 Teredo 整體的實現會比較複雜。
實驗環境搭建:
在 Linux 平台下有開源的 Teredo 實現版本:miredo。由於時間和文章篇幅的原因,而且部署 miredo 比較複雜,因此這裡的實驗等以後有機會再補充。
轉換技術(有一些文獻叫做:翻譯技術)
隧道技術是比較好地解決了在很長期一段時間內還是 IPv4 網路是主流的情況下 IPv6 節點(或者雙棧節點)間的通訊問題。但是由於 IPv4 到 IPv6 的過渡是十分漫長的,因此也需要解決 IPv6 節點與 IPv4 節點通訊的問題。協議轉換技術可以用來解決這個問題。
協議轉換技術根據協議在網路中位置的不同,分為網路層協議轉換、傳輸層協議轉換和應用層協議轉換等。協議轉換技術的核心思路就是在 IPv4 和 IPv6 通訊節點之間部署中間層,將 IPv4 和 IPv6 相互映射轉換。
我們非常熟悉的 NAT 也是一種典型的協議轉換技術,是將私網 IPv4 地址映射轉換為公網 IPv4 地址,這種轉換技術又稱為 NAT44。而我們接著要重點介紹的名為 NAT64/DNS64 的協議轉換技術。
NAT64/DNS64
提到 NAT64/DNS64,相信做 iOS 客戶端開發的同學一定非常熟悉。在 2016 年中開始,蘋果要求 app 必須支援 IPv6 網路。而蘋果官方提供的過渡解決方案正是 NAT64/DNS64。
以下是蘋果提供的技術圖:

圖33 蘋果提供的過渡技術解決方案
NAT64/DNS64 分為 NAT64、DNS64 兩大方面,兩者需要結合使用。
DNS64 在 RFC6147 中明確定義,將 IPv6 的地址記錄 AAAA DNS 查詢消息轉換為 IPv4 的地址記錄查詢。當 IPv6 節點發起 DNS 請求,NAT64/DNS64 中間層同時發起 A 域名查詢和 AAAA 域名查詢。如果僅有 A 域名查詢的 IPv4 地址響應,表明 IPv6 節點需要訪問一個 IPv4 的節點,NAT64/DNS64 中間層將回應的 IPv4 地址轉換為 IPv6 地址,返回給 IPv6 節點。
IPv6 節點使用獲取到的 IPv6 服務端地址進行訪問,數據包會經過 NAT64/DNS64 中間層,中間層將 IPv6 地址映射轉換為 IPv4 的地址進行訪問。
實驗環境搭建:
Linux 平台下有多個 NAT64 的開源軟體,實現方式各有不同,有純內核態實現的 ecdysis,也有用戶態實現的 tayga。
DNS64 的實現可以使用著名的開源 DNS 服務 BIND 就可以很好地支援,詳細可以查看上面 2 個開源軟體的搭建說明。
時間的原因,還沒有把 NAT64/DNS64 的開源軟體研究透徹,因此這裡的實踐等以後有機會再補上。
PS:在研究 tayga 和 miredo 源碼的時候,發現了在 Linux 平台上面有一些有趣的東西,如下圖,是 tayga 的軟體實現框架。

圖34 Linux下的一個有趣的虛擬設備
Linux 內核自帶了一個軟體虛擬設備,也是一種隧道的實現(/dev/net/tun),該設備可以實現將內核態的網路數據發送到用戶態,用戶態修改後再返回給內核態,用戶態的進程負責完成 NAT64 這一次「偷龍轉鳳」操作。
關於/dev/net/tun 設備的實現,可以查閱 Linux 內核源碼 drivers/net/tun.c,一些著名的 VPN 軟體例如 openvpn 等,都是以它作為實現基礎。
本章只介紹了一些典型的過渡技術,其實過渡技術種類還有很多,有一些在實驗室階段,有一些已經商用,有一些已經被廢棄,但是總的來說,每一種過渡技術都是在解決特定時期特定場景下的過渡問題。
IPv6 編程應該注意的問題
在《IPv6 Socket 編程》一文中,ray 已經很詳細介紹了 IPv6 下的 socket 編程細節和應該注意的問題。本章作為一個補充,介紹一下 IPv6 socket 編程可能還會遇到的問題。
1、IPv6 地址編碼
IPv4 地址本質是一個 32 位整數,因此一般無論是存儲層還是邏輯層,都經常將點分制的 IPv4 字元串地址轉為 32 位整數使用。而在 IPv6,情況就複雜多了(可能也有同學就想到,光是原子性就很難保證了)。
舉一個典型的例子,現在有個需求,分別統計每個 IP 的訪問頻次。
在 IPv4 的情況下,最簡單就是 STL 用 std::map 搞定(單執行緒),土豪一點的可以開個 16G 的數組用空間換時間。
但是在 IPv6 的場景下,那就尷尬了,IPv6 可是個 128 位整數,可以用 map 嗎?可能會有人直接將原始的字元串類型的 IPv6 地址作為 key 來累計。一旦那麼用,就要十分注意了。由於 IPv6 是支援前導 0 和連續 0 的壓縮表示方式,而且支援英文字母大小寫,例如:
2001:db8:4::41
2001:db8:4:0:0:0:0:0:41
2001:0db8:4::41
2001:DB8:4::41
這 4 個都是合法的 IPv6 地址,如果將輸入毫無修改地作為 key 來累計,那必須會將累計邏輯分散了,最終得不到正確的頻率結果。類似的問題也在 MAC 地址(BSSID)上面,由於 MAC 地址分號間的數字前導 0 可以省略,並且也是支援大小寫英文字母,所以也是會同樣的問題。在微信安全中心,MAC 地址的邏輯統一轉為 64 位整數處理,情況相對還好。
但是到了 IPv6 有木有更好的解決辦法呢?答案是肯定的,但是需要具體問題具體分析。
在上面的頻率例子比較優雅的做法,依然用 map 的話,可以利用自定義 key 類型解決,這個方法需要重載自定義類型的比較符號』<』:

圖35 自定義IPv6地址結構
其中 struct in6_addr 就是一個 128 位的 IPv6 地址結構體。

圖36 使用std::map實現IPv6頻率
其實還有更優雅的方式,直接將 IPv6 的地址強制轉為 2 個 64 位整數來比較,if else 會寫得更少一些,效率更高一些。
上面說到 2 個 64 位整數,微信安全中心有一些靜態的 key-value 數據查詢(批量寫,多次讀),其中 key 是 MD5,我們將 MD5 也是作為 2 個 64 位整數來對待,將 2 個 64 位整數聯合排序,寫入記憶體,然後使用兩次二分查找的方式搜索,效率非常高。在這種場景下面,IPv6 也是可以用類似的方法處理。
IPv6 地址結構,以後很可能會給我們的編程或多或少帶來一些「未知」的坑-_-||。
2、IPv6 socket「兼容」IPv4 的情況
在 IPv4 和 IPv6 共存的一個很長的時間裡,在 socket 編程上不得不面對的就是 IPv6 和 IPv4 一定程度的「兼容問題」。而在文章前面有提到,IPv6 和 IPv4 和完全不兼容的兩種協議,但是 IPv6 協議的地址空間更大,是可以使用 IPv6 的地址表示 IPv4 地址,例如 IPv4 映射地址,因此,在很特殊的情況下,IPv4 和 IPv6 可以實現「兼容」,但是這種兼容是很有限的。在 Linux 平台下,這種「兼容性」是如何表現的,我們這裡來分析一下。
在 Linux 下面,以 IPv6 下的 UDP Socket 舉例:
有個 UDP 協議的 Server 改造 IPv6,該 Server 機器上有一個網卡並且同時配置 IPv6 和 IPv4 地址,支援雙棧。Server 進程創建 IPv6 UDP socket 套接字,綁定 Server 本地任意地址(IPv4 和 IPv6 都是以全 0 地址為綁定任意地址)。客戶端是 IPv4,向這個 Server 發送 UDP 請求數據包。

圖37 IPv6服務收到IPv4報文
可以看到的是,IPv6 的 socket 會正常收到客戶端的數據報文,並且會將 IPv4 地址轉化為映射地址,為了明確這個邏輯,我們分析 Linux 內核的實現。

圖38 IPv6下UDP socket收到IPv4數據包內核實現
IPv6 的 socket 收到數據包,如果是 IPv4 協議,則將來源 IPv4 的 IP 地址轉為 IPv6 的 IPv4 映射地址。與實驗的結果很一致。
如果 Server 的 IPv6 socket 按照這個來源地址返回數據包,那麼內核又是如何處理的呢?

圖39 IPv6下UDP socket發送IPv4數據包內核實現
首先內核會判斷目的地址是否為 IPv6 的 IPv4 映射地址,如果是映射地址,那麼要發送的數據是 IPv4 數據,直接以 IPv4 協議棧的形式發送該數據(udp_sendmsg 是 IPv4 udp 發送介面)。
可以看到,Linux 內核本身對這類雙棧上的改造做了一定的適配,我們可以根據內核的這種特性去進行改造工作。
3、使用鏈路本地地址
從前面的章節可以知道,IPv6 具有自動配置地址的能力。鏈路本地地址是 IPv6 要求在每個介面默認自動配置生成的地址,用於鏈路上的通訊,路由器不能轉發鏈路本地地址。除了以上提到的特徵外,鏈路本地地址就是一個普通的 IPv6 地址,我們可以使用這類地址做 socket 編程通訊。
但是我們在 IPv6 Socket 編程的時候使用鏈路本地地址,有一個細節需要注意。

圖40 IPv6地址結構
在 IPv6 地址結構中(對應於 IPv4 的 struct sockaddr_in),有一個我們非常陌生的欄位 scope_id,這個欄位在我們使用鏈路本地地址來編程的時候是必須要使用的,這個欄位表示我們需要選擇介面 ID。為什麼需要需要有這麼一個欄位,那是因為鏈路本地地址的特殊性,一個網路節點可以有多個網路介面,多個網路介面可以有相同的鏈路本地地址,例如我們需要 bind 一個本地鏈路地址,這個時候就會有衝突,作業系統無法決策需要綁定的是哪個介面的本地鏈路地址。
又例如,如果我們在直連的 2 個主機之間直接用鏈路本地地址 ping 的話,會 ping 失敗。
因此 IPv6 引入了 scope_id 來解決這個問題,scope_id 指定了使用哪個網路介面。
如何查看這個網路介面(網卡)的 scope_id 是多少?
一、在 Linux 下查看網路介面的 scope_id:

圖41 Linux下查看網路介面scope id
使用 ip addr 命令可以查看每個介面的 scope_id,如圖第一列的數字就是 scope_id。
二、在 windows 下查看 scope_id:

圖42 Windows下查看網路介面scope id
最後的百分號%後面的數字就是該網路介面的 scope_id。
Windows 下也可以使用 route print -6 查看介面列表,列表第一列數字就是 scope_id。
因此,在使用鏈路本地地址編程的時候,需要把這個 scope_id 賦值到 sin6_scope_id 欄位。
而在使用 ping 命令的時候,需要在地址後面加上%和 scope_id 才能 ping 成功,如圖:

圖43 使用鏈路本地地址ping
關於這個 scope id,詳細可以查看 RFC2553
總結
本文主要科普介紹了 IPv6 的基本內容,配合各種實驗分析比較清晰認識了 IPv6 的各種基本概念;也介紹一些「超綱」的內容(我們的工作中很可能不會接觸到),但是我覺得這類內容在技術實現上十分有趣,可以在一些技術的方法和思路上面可能會給我們一些通用的啟示,例如 NAT64/DNS64 就是使用中間層來處理 IPv4 和 IPv6 互通的問題,我們的工作中也確實經常遇到類似的技術問題。
IPv6 本身是一個很龐大的體系,還有很多高級內容沒有介紹(IPv6-IPSec、移動 IPv6 等等)。而且查看和 IPv6 相關的 RFC,不斷在做修正,Linux 內核的 IPv6 模組程式碼也不斷有配合新的 RFC 修改來做調整,引入新的邏輯,以適應各種場景的實際需求。有興趣的同學可以一直留意 RFC 的變化和緊跟 Linux 內核的版本發布。
本文是結合各種文獻和實驗對 IPv6 理解的一個總結歸納,難免會有理解偏差和手抖的地方,希望各位同學熟悉的話能幫忙指出其中的錯誤,並且提供修改建議和意見,謝謝。
