閒談IPv6-v4/v6協議轉換報文的checksum無關性
版權宣告:本文為博主原創,無版權,未經博主允許可以隨意轉載,無需註明出處,隨意修改或保持可作為原創! https://blog.csdn.net/dog250/article/details/89039395
在IPv6時代,是不是可以用本地鏈路質量資訊編碼源地址的主機識別符號從而指導伺服器端擁塞控制策略呢,是不是也可以把自己是誰編碼進去呢?比如自己是Android,自己是一臺PC,或者說自己是一雙智慧皮鞋?以此來指導資料傳送端的定製化動作呢?
IPv6的地址空間足夠大,且留下了可達64位的主機識別符號可供任意發揮,如此長度的主機識別符號可以藏匿很多資訊啊!
可以先看一下我很久之前在2012年寫的一篇文章:
IPv6的NAT原理以及MAP66: https://blog.csdn.net/dog250/article/details/7799398
很有意思。
這種 利用IPv6地址空間遠大於IPv4地址空間的特性,在IPv4報文轉換為IPv6報文實現IPv4和IPv6之間互訪的時候,通過解一個一元一次方程來保證協議checksum無需重新計算 的技術,其實還有很多玩法。
關鍵不在於什麼解一元一次方程,而是在於IPv6的地址空間比IPv4地址空間足夠大,在IPv4地址嵌入到IPv6地址中後,剩餘的空間仍然可以儲存校驗碼矯正值。
之前說過,IPv4和IPv6之間存在聯通的必要,因為要平滑過渡就必然需要某種相容,那麼這種聯通就可以分為兩類:
- 橫向聯通: IPv4海洋中,IPv6孤島之間的互訪,此時需要IPv4隧道,參見6to4以及ISATAP等。
- 縱向聯通: IPv4直接訪問IPv6資源或者反過來。此時就需要協議轉換,協議轉換必然涉及到checksum的重新計算問題。
以上縱向聯通方面,有一個超猛支撐技術,那就是DNS64,但是這種DNS技術更加側重於管理平面和配置技巧,不是我的菜,所以我也不想多聊,分享一篇文章:
支援IPv6 DNS64/NAT64 網路<- 網路概述: https://www.jianshu.com/p/37b8c006cd2d
為了防止連結失效,盜圖一張,解釋DNS64:

邏輯是比較簡單的,但細節卻足夠繁瑣,超級煩人。
本文不想談DNS64,本文談談當IPv4報文轉換為IPv6報文以及IPv6報文轉換為IPv4報文時,上層協議checksum的計算問題。
上層協議在計算checksum時早就不需要IP層欄位作為偽頭參與了,但是不管TCP,UDP還是ICMP都是古老的協議,它們設計時就如此,沒有辦法,即便是IPv6還是要支援!
如果在協議轉換的集中化節點去重新計算上層協議的checksum,那麼資源的消耗將會是集中式的,為此,我們希望這些相關的計算儘量在邊緣進行。此外,由於IPv6沒有NAT或者至少說不提倡NAT,且地址足夠長,沒有哪臺裝置有足夠的記憶體可以承受海量的連線狀態跟蹤的維護,所以需要某種stateless機制去維護conntrack!
就像TCP的Syncookie一樣,我們 可以把conntrack資訊存放在IPv6報文字身,因為它的地址空間足夠大!
先看IPv4報文轉換為IPv6報文時,如何保持上層checksum的不變性。
按照RFC6052的規範:
IPv6 Addressing of IPv4/IPv6 Translators: https://tools.ietf.org/html/rfc6052
IPv4地址會嵌入到IPv6地址空間的低位,具體就是下面這個規則了(參見 2.2節 ):

注意後面的suffix,這些字尾空間是可以供我們自由發揮的。
除卻96位的prefix實在是沒有空間,其它的情況,至少可以在suffix低位取2個位元組來存放checksum矯正值,計算這個checksum矯正值的問題可以描述為:
求解一個16bit的數字,請問它是多少時,當IPv4頭按照RFC6052規範換成IPv6頭時,TCP的checksum可以保持不變?
這不就是一個一元一次方程嘛…
給出一段程式碼:
#include <stdio.h> #include <stdlib.h> #include <string.h> //以下2個函式就是計算校驗碼的,具體的原理請參見RFC1071/RFC1624/RFC1141 static inline u_int16_t add16( u_int16_t a, u_int16_t b) { a += b; return a + (a < b); } static inline u_int16_t csum16(const u_int16_t *buf, int len) { u_int16_t csum = 0; while(len--) csum = add16(csum, *buf++); return csum; } unsigned char sip[4] = {192, 168, 1, 1}; unsigned char dip[4] = {172, 16, 2, 2}; // 轉換後的源IPv6地址,即將源IPv4地址嵌入到IPv6地址後64位的高32位,sip6已經完成嵌入 unsigned char sip6[16] = {0x20, 0x01, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 192, 168, 1, 1, 0x00, 0x00, 0x00, 0x00}; // 1. 轉換後的目標IPv6地址,即將目標IPv4地址嵌入到IPv6地址後64位的高32位,sip6已經完成嵌入 // 2. 目標IPv4地址2.2.2.2,是由DNS64機制通告給IPv4-only的客戶端的,到達NAT64閘道器後,將其轉換為下面的IPv6地址 unsigned char dip6[16] = {0x20, 0x01, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00, 172, 16, 2, 2, 0x00, 0x00, 0x00, 0x00}; // 簡版IPv4頭 struct ipv4hdr { unsigned char sip[4]; unsigned char dip[4]; }; // 簡版IPv6頭 struct ipv6hdr { unsigned char sip6[16]; unsigned char dip6[16]; }; // 報文格式:|0~31 IPv4/6頭部|32~45 payload|46~47 校驗碼| int main(int argc, char **argv) { u_int16_t csum, *c2; // 完整的資料報文:0~31bit可以裝下簡版IPv6頭,24~31bit可以裝下簡版IPv4頭.保持payload不變,方便操作 unsigned charpacket[48] = {0}; // 暫存IPv6頭 unsigned charhdr6buf[32] = {0}; struct ipv4hdr *hdr4; struct ipv6hdr *hdr6; hdr4 = (struct ipv4hdr *)&packet[24]; memcpy(hdr4->sip, sip, 4); memcpy(hdr4->dip, dip, 4); // 10個位元組的payload memcpy((unsigned char *)hdr4 + sizeof(struct ipv4hdr), "1234567890abcd", 14); // 最後2個位元組儲存校驗碼 memset((unsigned char *)hdr4 + sizeof(struct ipv4hdr) + 14, 0, 2); // 一共校驗23個16位組,總共46個位元組 csum = csum16((u_int16_t *)(&packet[0]), 23); c2 = (u_int16_t *)(&packet[46]); *c2 = csum; printf("IPv4報文資料檢驗碼(可模擬包含偽頭的TCP校驗碼):%.2X\n", csum); { int i = 0; printf("begin IPv4 packet:\n"); for (i = 24; i < 48; i++) { printf ("%.2X ", packet[i]); } printf("\nend IPv4 packet\n"); } hdr6 = (struct ipv6hdr *)&hdr6buf[0]; memcpy(hdr6->sip6, sip6, 16); memcpy(hdr6->dip6, dip6, 16); u_int16_t csum_hdr6 = csum16((u_int16_t *)hdr6, 16); //定位固定修改後的動態修改的初始地址,我將IPv6頭的源地址hostID的最低2個位元組用於檢驗碼矯正! u_int16_t* pcsum = (u_int16_t*)(&hdr6buf[14]); //計算校驗碼矯正值!即已知h,p,c,求x:h + p + x = c *pcsum = ~add16( add16( ~(*pcsum), ~csum16((u_int16_t*)(&packet[0]), 16) ), csum_hdr6 ); printf("校驗碼矯正值為:%X\n", *pcsum); memcpy(&packet[0], &hdr6buf[0], 32); //完成修改 printf("當前IPv6報文的校驗碼(模擬在IPv4頭轉換為IPv6頭之後,TCP協議的校驗碼不需要改變):%.2X\n", csum16((u_int16_t*)(&packet[0]), 23)); { int i; printf("begin IPv4 packet:\n"); for (i = 0; i < 48; i++) { printf ("%.2X ", packet[i]); } printf("\nend IPv4 packet\n"); } return 0; }
編譯執行之:
[root@localhost DESTHDR]# ./a.out IPv4報文資料檢驗碼(可模擬包含偽頭的TCP校驗碼):883E begin IPv4 packet: C0 A8 01 01 AC 10 02 02 31 32 33 34 35 36 37 38 39 30 61 62 63 64 3E 88 end IPv4 packet 校驗碼矯正值為:EEB0 當前IPv6報文的校驗碼(模擬在IPv4頭轉換為IPv6頭之後,TCP協議的校驗碼不需要改變):883E begin IPv4 packet: 20 01 00 01 02 03 04 05 C0 A8 01 01 00 00 B0 EE 20 01 05 04 03 02 01 00 AC 10 02 02 00 00 00 00 31 32 33 34 35 36 37 38 39 30 61 62 63 64 3E 88 end IPv4 packet
是不是很好玩?解一元一次方程也能解出實際用途來。
這可不是我自己說的,這可是RFC上說的,我只是照著試試做一下而已:
https://tools.ietf.org/html/rfc6052#section-4.1現在反過來,IPv6報文如果轉換為IPv4報文呢?
我們知道IPv4報文字身就有一個針對於IPv4協議頭的校驗碼欄位,如果資料始發於IPv6棧,那麼當它需要轉換為IPv4報文時,看樣子這個IPv4的校驗碼計算是躲不過。
確實躲不過,但問題是在哪裡進行這個計算。是在邊緣節點還是在協議轉換的節點來做呢?
我認為可以在邊緣節點來做這個計算,然後把值藏匿於IPv6地址的 可以自由發揮的主機識別符號裡 就是了。
假設從IPv6棧發起一個去往IPv4地址10.18.19.2的資料報文,按照RFC6052的規範,這個IPv4地址肯定被編碼進了IPv6棧的源地址的主機識別符號裡,那麼是不是可以在資料始發的時候,就直接按照IPv4地址來計算TCP/UDP/ICMP的校驗碼呢,然後繼續計算IPv4頭的校驗碼,將IPv4頭的校驗碼藏匿於源IPv6地址的suffix即可。
下面是一個例子:
- IPv6始發:2001: 1234: 1234: 1234:192.168.12.2::0/64到2001:4321:4321:4321:172.16.12.2::/64。
- 始發站直接按照192.168.12.2和172.16.12.2作為源和目標計算4層協議checksum儲存在報文checksum欄位。
- 始發站自行組裝源和目標分別為192.168.12.2和172.16.12.2的IPv4報頭,計算IPv4的checksum,保存於源IPv6地址2001: 1234: 1234: 1234:192.168.12.2::0/6的最後2個位元組。
- 協議轉換閘道器收到報文,按照嵌入的IPv4地址組裝IPv4頭,取出IPv6源地址的低2位元組作為checksum裝入IPv4頭的checksum欄位。
- IPv4報文發出到IPv4網路。
這便解放了協議轉換閘道器的算力資源。
遺留的問題是,IPv6始發站如何識別一個報文是不是發往IPv4網路的,如何觸發它去按照內嵌IPv4地址去生成偽頭以及去計算一個IPv4頭的校驗碼,這除了RFC6052之外,就看應用層的配置了,一個sockopt規則灌下去,非常容易做到!
既然IPv4和IPv6要互聯互通,肯定是需要協議轉換裝置了,我本來就是一個裝置迷而不是很care什麼軟體,所以,本文也是看了下面的新聞才有感而發的:
國內首個IPv6翻譯裝置認證出爐 北京英迪瑞訊IVI通過IPv6 認證: http://www.qianjia.com/zhike/201904/031822517170.html
我可能思想過於老套了,但我依然覺得裝置才是重要的,軟體灌入裝置賣出去才有效。最關鍵的,我不喜歡網際網路軟體的原因是,網際網路軟體的程式碼一般都很low,畢竟伺服器都在這些網際網路公司自己的機房,出了問題遠端登入即可排錯,而裝置是賣出去到客戶那裡的,排錯成本高昂,所以必須精雕細琢不斷測試。
清明時節,沒有雨紛紛,所以皮鞋就不會溼,所以也就更加不會胖。