IPv6出來已經很多年,雖然距離普及還很遠,但項目里要加上,有沒有人用是一碼事,但不加上肯定過不了審。IPv6最大的問題是包格式與IPv4不兼容
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version| IHL |Type of Service| Total Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Identification |Flags| Fragment Offset |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Time to Live | Protocol | Header Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Destination Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options | Padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version| Traffic Class | Flow Label |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Payload Length | Next Header | Hop Limit |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
+ +
| |
+ Source Address +
| |
+ +
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
+ +
| |
+ Destination Address +
| |
+ +
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
作為一個程序員,粗略一看,這個應該是兼容的啊。畢竟前4bit都是版本號,拿到數據包時,判斷一下版本號,根據不同的版本號做不同的處理,即可做兼容,很多軟件都是這么做的。然而問題是,IP數據包涉及的不僅僅是軟件,還有硬件,比如路由器。當一個數據包經過路由器時,它需要解析包頭里的數據,得到目標地址,才知道數據包轉發到哪里。如果這個包頭格式不一樣了,那這個數據包就無法正常轉發。對於軟件,可以重新發布一個版本來解決,但絕大部分的家用路由器,是不帶更新功能的,那要讓它支持Ipv6,只能扔掉買一個新的。在現實生活中,一個數據包的傳輸,可能會經過很多路由的,例如
客戶端 --------> 家庭路由 --------> 小區路由 --------> ISP主路由 --------> 服務器
當這個問題擴大到整個社會,就會有無數的家庭路由,無數的小區路由。ISP的主路由和服務器由於商業的驅動,可以及時更新,而無數的家庭路由,無數的小區路由,顯然沒法短時間內全部更新,也沒必要更新。
目前的情況是,客戶端、路由器都有可能支持IPv6,也有可能都不支持,因此IPv4和IPv6的兼容是必須得做的,不然就可能失去一部分用戶了。這個兼容是指“不管客戶端以及所經過的路由是IPv4還是IPv6,不管服務器是IPv6還是IPv4,兩者都能正常進行交互”。排除正常的條件((如:客戶端為IPv6,服務器也是IPv6)的情況,需要特殊處理的情況為IPv4訪問IPv6和IPv6訪問IPv4。
NAT64 & DNS64
既然IPv6和IPv4兼容性是由它們的IP報文不一樣,那么可以弄一個專門轉換報文的服務,即NAT64(Network Address Translation IPv6 to IPv4)。而一個IPv6客戶端想把一個報文發往一個IPv4服務器,就需要一個包含IPv4地址的IPv6地址,這由一個特殊的DNS服務提供,即DNS64。
假如現在有一台IPv4的服務器,因無人維護無法升級到IPv6,但用戶已更換了新手機,新路由器,升級到了IPv6。那這時候需要給這台服務器配置NAT64和DNS64,否則用戶就訪問不了了。
- 運維部署一台NAT64服務器(自建,也可以是雲服務商提供的服務器),配置好服務器的IPv4地址(以192.168.0.100為例)和IPv6前綴(一般是64:ff9b::/96 )
- 運維在DNS64服務器(DNS64服務器可能是自建,也可能是公共DNS64服務器,也可以是雲服務商提供的服務器)把域名指向NAT64服務器的IPv4地址(以192.168.0.101為例)及前綴(必須和NAT64配置的前綴一致)
- IPv6客戶端需要和服務器通信,根據域名發起IPv6 DNS查詢,由於服務器不支持IPv6,所以沒有查詢到。但是DNS64服務器發現了一個IPv4的地址,於是把這個IPv4加上前綴,得到一個IPv6地址,即
64:ff9b::192.168.0.101,於是客戶端往這個地址發請求。 - NAT64收到IPv6數據包,會解析IP地址的前綴,發現和運維配置的前綴一致,於是把這個數據包轉換為一個IPv4數據包,按NAT規則往配置好的服務器地址發送IPv4數據包
- IPv4服務器收到IPv4數據包,返回IPv4數據包
- NAT64收到服務器返回的數據包,按NAT規則轉換為IPv6數據包,返回給IPv6客戶端,完成了交互
DNS64
^ |
| |
| v
IPv6客戶端 --------> IPv6路由器 --------> NAT64(192.168.0.101) --------> IPv4服務器(192.168.0.100)
NAT64原本設計的目的是讓IPv6客戶端訪問IPv4,而不是IPv4訪問IPv6。因為IPv4是舊標准,既然服務器支持IPv6,說明是從IPv4升級而來的,那保留IPv4功能即可,一般用不着NAT轉換。但事情也不是這么絕對,因此雖然很少有人提及,但還是有NAT46這種東西的,見:NAT64 - NAT46。
- 運維部署NAT46服務器
- 運維在DNS46把域名指向NAT46服務器(NAT46服務器最好有一個公網IPv4地址,沒有的話據說DNS46會做一個臨時mapping,我覺得是個大問題,除非部署在私有網絡)
- IPv4客戶端查詢域名得到一個IPv4地址,往這個地址發一個IPv4數據包
- NAT46服務器收到一個IPv4數據包,把這個IPv4的地址加上一個前綴得到IPv6地址,然后按NAT規則往服務器發送IPv6數據包
- IPv6服務器收到IPv6數據包,正常返回IPv6數據包
- NAT46服務器收到返回的IPv6數據包,轉換為IPv4數據往,返回給IPv4客戶端
DNS
^ |
| |
| v
IPv4客戶端 --------> IPv4路由器 --------> NAT46(192.168.0.101) --------> IPv6服務器
可以看到,NAT46其實是NAT64反過來的,實際上它和NAT64服務器回包的流程基本一致,因此大部分NAT64服務配置一下,都能支持NAT46,比如NE40E。
而對於DNS,雖然有純IPv4的DNS,也有純IPv6的DNS,但是一般都會升級為DNS64,即可支持IPv4的域名查詢,也可以支持IPv6的查詢,例如114.114.114.114是一個很常見的IPv4 DNS服務器,但也可以返回IPv6的地址
~$ nslookup -type=AAAA www.google.com 114.114.114.114
Server: 114.114.114.114
Address: 114.114.114.114#53
Non-authoritative answer:
Name: www.google.com
Address: 2001::4a7d:278a
NAT64是在運維層面去解決兼容性,不需要程序去做兼容。但是部署NAT64是需要成本的,你可以買硬件,也可以從雲服務那里買整套服務,但這都是白花花的銀子啊。對於大部分公司而言,他們的產品是有程序員去維護,可以從程序做兼容,而不需要花這一筆錢。
Dual Stack
從另一個角度來看,既然IPv4和IPv6不兼容,那就沒必要讓它們兼容。可以同時開啟IPv4和IPv6,這樣各走各的路,互不干擾。那這就需要同時實現IPv4和IPv6協議棧,即雙棧(Dual Stack)。
現在新的路由和和操作系統基本都是支持雙棧的,例如ubuntu 20.04下通過ip a可以查看
~$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: enp2s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether b4:b5:2f:91:fe:21 brd ff:ff:ff:ff:ff:ff
inet 192.168.3.6/24 brd 192.168.3.255 scope global dynamic noprefixroute enp2s0
valid_lft 73235sec preferred_lft 73235sec
inet6 fe80::f2a1:25a2:d365:468c/64 scope link noprefixroute
valid_lft forever preferred_lft forever
可以看到,網卡2有兩個地址:inet 192.168.3.6和inet6 fe80::f2a1:25a2:d365:468c,說明支持雙棧。不過這里不打算細說這些設備,作為一個程序員更關注的是怎么實現雙棧程序。
最簡單的辦法,同時開兩個socket,一個IPv4,一個IPv6,問題是這樣寫代碼有點復雜。在IPv6出來的時候,在系統的底層庫提供了雙棧的接口,直接調用就可以了。
- 服務端
// 以IPv6進行監聽
int fd = ::socket(AF_INET6, SOCK_STREAM, IPPROTO_IP);
// 把ipv6 only關掉,這樣會同時開啟兩個監聽,一個IPv4,一個IPv6
// 允許v4的連接以 IPv4-mapped IPv6 的形式連進來
int optval = 0;
if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, (char *)&optval, sizeof(optval)) < 0)
{
return -1;
}
struct sockaddr_in6 sk_socket;
memset(&sk_socket, 0, sizeof(sk_socket));
sk_socket.sin6_family = AF_INET6;
sk_socket.sin6_port = htons(port);
// 使用新的接口inet_pton,它即支持IPv4,也支持IPv6,所以host可以傳::ffff:127.0.0.1,
// 也可以傳fe80::f2a1:25a2:d365:468c,但不能直接傳127.0.0.1,可以用getaddinfo轉換
if (inet_pton(AF_INET6, host, &sk_socket.sin6_addr) < 0)
{
return -1;
}
if (::bind(fd, (struct sockaddr *)&sk_socket, sizeof(sk_socket)) < 0)
{
return -1;
}
if (::listen(fd, 256) < 0)
{
return -1;
}
return 0;
- 客戶端
// 創建IPv6連接
int fd = ::socket(AF_INET6, SOCK_STREAM, IPPROTO_IP);
struct sockaddr_in6 sk_socket;
memset(&sk_socket, 0, sizeof(sk_socket));
sk_socket.sin6_family = AF_INET6;
sk_socket.sin6_port = htons(port);
// 使用新的接口inet_pton,host可以傳::ffff:127.0.0.1,
// 也可以傳fe80::f2a1:25a2:d365:468c,但不能直接傳127.0.0.1,可以用getaddinfo轉換
if (inet_pton(AF_INET6, host, &sk_socket.sin6_addr) < 0)
{
return -1;
}
if (::connect(fd, (struct sockaddr *)&sk_socket, sizeof(sk_socket)) < 0)
{
return -1;
}
return 0;
- DNS查詢
int32 Socket::get_addr_info(std::vector<std::string> &addrs, const char *host)
{
struct addrinfo hints;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET6;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_V4MAPPED; // 目標無ipv6地址時,返回v4-map-v6地址
hints.ai_protocol = 0; /* Any protocol */
hints.ai_canonname = NULL;
hints.ai_addr = NULL;
hints.ai_next = NULL;
struct addrinfo *result;
if (0 != getaddrinfo(host, nullptr, &hints, &result))
{
return -1;
}
char buf[INET6_ADDRSTRLEN];
for (struct addrinfo *rp = result; rp != NULL; rp = rp->ai_next)
{
if (inet_ntop(rp->ai_family,
&((struct sockaddr_in6 *)rp->ai_addr)->sin6_addr, buf,
sizeof(buf)))
{
addrs.emplace_back(buf);
}
else
{
return -1;
}
}
freeaddrinfo(result);
return 0;
}
上面的代碼是從我的工程里拷貝而來,不一定能直接運行,僅供參考,詳見原文件socket.cpp。執行服務端的程序后,可以用telnet測試效果
root@debian:/home# netstat -lp | grep master
tcp6 0 0 [::]:8182 [::]:* LISTEN 1458/./master
root@debian:/home# telnet 127.0.0.1 8182
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
^C^]
telnet> quit
Connection closed.
root@debian:/home# telnet -6 ::1 8182
Trying ::1...
Connected to ::1.
Escape character is '^]'.
^]
telnet> quit
Connection closed.
root@debian:/home#
PS
-
用
::1去監聽時,只能通過::1來連接,用127.0.0.1是不行的。用::監聽則可以用::1或者127.0.0.1連接 -
Linux下,同一個程序以Dual Stack模式監聽時,netstat只顯示IPv6的監聽(上面telnet測試就只grep到一個記錄),而部分程序(如
sshd)則顯示兩個,猜測可能是開了兩個socket監聽而不是用雙棧實現。而Win下同一個程序則是顯示兩個記錄。
# linux
root@debian:/home# netstat -lp | grep sshd
tcp 0 0 0.0.0.0:ssh 0.0.0.0:* LISTEN 595/sshd
tcp6 0 0 [::]:ssh [::]:* LISTEN 595/sshd
# win
netstat -aob
活動連接
協議 本地地址 外部地址 狀態 PID
TCP 0.0.0.0:27015 DESKTOP-KR5SLB1:0 LISTENING 7812
[winipv6.exe]
TCP [::]:27015 DESKTOP-KR5SLB1:0 LISTENING 7812
[winipv6.exe]
Linux的這個設定一度讓我以為我的程序沒有監聽成功,查了好久Bug。也有人遇到同樣的問題,不過官方沒有確認為bug,見https://bugs.launchpad.net/ubuntu/+source/net-tools/+bug/657270
-
雙棧雖然可以接受IPv4連接,但實際上是以IPv6模式來處理的,因此獲得到的IP地址為
::ffff:192.0.2.1這種IPv4-mapped IPv6的格式,而不是原生的IPv4地址 -
::ffff:127.0.0.1雖然是一個IPv6地址,但它不是::1,而是是等同於127.0.0.1。即監聽::ffff:127.0.0.1時,只能從127.0.0.1連接,用::1是連不上的。如果開啟IPV6_V6ONLY,無法監聽這個地址的,在win下拋出一個1049錯誤。我猜測IPv6這個協議段都是用來做兼容的,原生的IPv6不使用這個號段。 -
::ffff:c000:201和::ffff:192.0.2.1這兩個地址是相同,把::ffff:c000:201用getaddrinfo解析,得到的就是::ffff:192.0.2.1,它們的轉換算法:
IP 192.0.2.1 的十六進制轉換為0xC0000201
192 十六進制是 c0
0 十六進制是 00
2 十六進制是 02
1 十六進制是 01
可以在線直接轉換:IPv6 to IPv4 IPv4 to IPv6
客戶端的連接處理
服務端如果需要對外部署的話,無論是自建服務器還是雲服務器,在部署的時候都能夠知道是否支持IPv6,但客戶端並是不能確定的。公司可以發布一個支持IPv6的客戶端,但即使查詢IPv6的DNS成功,也無法保證客戶端到服務器之間的所有設備都支持IPv6,DNS查詢成功只是表示客戶端到DNS服務器之間的IPv6鏈路是通的,與服務器不是同一條鏈路,直接用IPv6去連服務器可能會失敗。IETF推薦的做法是:優先查詢IPv6地址,然后是IPv4地址,一個個嘗試去連接。沒看錯,就是for循環一個個去試。
域名檢測
想測試一個域名是否支持IPv6,有很多工具,比如nslookup -type=AAAA www.google.com 114.114.114.114,也可以在線測試,例如https://www.boce.com/ipv6/
