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/


浙江温州皮鞋湿,下雨进水不会胖。

你可能感兴趣的:(IPv6,AF_INET6)