IPv6 socket偵聽in6addr_any的問題

當我們 netstat -lnt 查看本機偵聽端口的時候,經常會看到類似下面的展示:

tcp6       0      0 :::22                   :::*                    LISTEN      658/sshd: /usr/sbin

顯然,sshd創建了一個IPv6 socket,在in6addr_any地址上偵聽22號端口。

此時,我用一個該機器的IPv4地址去連接22號端口,通還是不通呢?爲了避開無關的討論,我假設net.ipv6.bindv6only的值爲0。

當然是通的,不信你試試。想知道細節上Why的去看源碼好了,這塊代碼很簡單。我這裏想引出一個和reuseport有關的問題。

按照TCP的語義,偵聽一個端口這件事和IP地址無關,僅僅和端口有關,按照socket的語義,bind一個地址需要同時提供IP地址和端口。

因此,在實現上,我們要區分開哪些是TCP規定的,哪些是socket規定的:

  • socket必須按照地址族進行分類,比方說IPv4 socket,IPv6 socket。
  • 偵聽某個端口的TCP不能區分連接來自IPv4地址還是IPv6地址。

IPv4 socket是AF_INET族,IPv6 socket是AF_INET6族,我討厭術語,就不說這些了。

在實現上,Linux顯然用同一張hash表保存包括IPv4,IPv6在內的所有偵聽socket,無論是IPv4還是IPv6的偵聽socket,在bind的最終,均會以其bind的端口爲鍵值插入到同一張hash表中,注意,這張hash表和IP地址完全無關。

當TCP連接到來的時候,協議棧會提取數據包的目標端口,以此爲鍵值來查詢唯一的這張保存偵聽socket的hash表,我們假設找到了一個匹配的IPv6 socket,並且該socket bind的是in6addr_any地址,那麼問題來了:

  • 如果來源連接是一個IPv6報文,顯然是可以成功建立連接的。
  • 如果來源連接是一個IPv4報文,能不能讓它建立連接呢?

這就要看如何理解 in6addr_any地址 了,即 "0:0:0:0:0:0:0:0" 這個IPv6地址包括不包括IPv4的 "0.0.0.0" ,對於Linux系統,在 bindv6only 關閉的情況下,答案顯然是肯定的。所以,當一個IPv6 socket在bind in6addr_any之後偵聽的話,無論是使用IPv4還是使用IPv6,均可以成功建立連接。

比如我用以下的代碼bind了一個IPv6地址:

inet_pton(AF_INET6, "0:0:0:0:0:0:0:0", (struct sockaddr_in6 *)&srvaddr.sin6_addr);
srvaddr.sin6_port = htons(1234);
bind(lsd, (struct sockaddr*)&srvaddr, sizeof(srvaddr));
listen(lsd, 10);

然後我用一個IPv4地址去連接:

telnet 192.168.56.101 1234

偵聽端接受連接請求後會將來源地址解析成來源IPv4地址的IPv4-Mapped地址 "::ffff:192.168.56.102" 你用netstat去查看該連接,顯示的依然是IPv4連接:

tcp6       0      0 192.168.56.101:1234     192.168.56.102:52802    ESTABLISHED 29047/./a.out

現在細節已經很清楚了,問題是,在保留bindv6only爲0的前提下,如何讓IPv6 socket不再接受IPv4的連接呢?

倒也不難,方法是:

  • IPv6 socket啓用reuseport,再創建一個IPv4 socket,bind到0.0.0.0的同一個端口即可。

如此一來,即使是IPv6 socket在in6addr_any上偵聽,它也不會接受IPv4的連接了,IPv4的連接完全由IPv4 socket來處理。

這個在Linux的實現中非常有意思,因爲它太簡單了。簡單說就是,對於偵聽同一個端口的情況:

  • IPv6 socket插入到hash鏈表的末尾。
  • IPv4 socket插入到hash鏈表的頭部。
  • 偵聽socket的查找從hash鏈表頭部開始遍歷。

顯然,偵聽同一個端口的IPv4 socket和IPv6 socket不可能在同一個reuseport組,它們只能按照自己在鏈表中的位置被遍歷。因此,如果來了IPv4的連接請求,在遍歷到IPv6 socket之前,首先會命中IPv4 socket。

簡單的邏輯不必說太多。

OpenBSD與Linux不同,它不允許IPv4報文被IPv6 socket處理,即便該IPv6 socket已經bind了in6addr_any地址,也要各管各的。OpenBSD的這種實現方式與Linux相比,到底是更簡單了還是更復雜了呢?

不得而知。

本來是還想再聊聊IPv4-Mapped地址的,特別是和安全相關的issue,但是時間不允許了,詳情看這裏:https://lwn.net/Articles/8646/


浙江溫州皮鞋溼,下雨進水不會胖。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章