当我们 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规定的:
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地址,那么问题来了:
这就要看如何理解 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在in6addr_any上侦听,它也不会接受IPv4的连接了,IPv4的连接完全由IPv4 socket来处理。
这个在Linux的实现中非常有意思,因为它太简单了。简单说就是,对于侦听同一个端口的情况:
显然,侦听同一个端口的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/
浙江温州皮鞋湿,下雨进水不会胖。