使用 IPv6 套接字读取 MLDv2 查询

Reading MLDv2 queries using an IPv6 socket

我在 raspberry pi 上安装了 mrd6。它向本地接口 (tun0) 注册并定期通过它传输 MLDv2 查询。

根据 [RFC3810], MLDv2 message types are a subset of ICMPv6 messages, and are identified in IPv6 packets by a preceding Next Header value of 58 (0x3a). They are sent with a link-local IPv6 Source Address, an IPv6 Hop Limit of 1, and an IPv6 Router Alert option [RFC2711] 中的 Hop-by-Hop 选项 header。

我可以确认我在 tun0 上定期看到这些数据包:

pi@machine:~ $ sudo tcpdump -i tun0 ip6 -vv -XX

01:22:52.125915 IP6 (flowlabel 0x71df6, hlim 1, next-header Options (0)
payload length: 36) 
fe80::69bf:be2d:e087:9921 > ip6-allnodes: HBH (rtalert: 0x0000) (padn)
[icmp6 sum ok] ICMP6, multicast listener query v2 [max resp delay=10000]
[gaddr :: robustness=2 qqi=125]
            0x0000:  6007 1df6 0024 0001 fe80 0000 0000 0000  `....$..........
            0x0010:  69bf be2d e087 9921 ff02 0000 0000 0000  i..-...!........
            0x0020:  0000 0000 0000 0001 3a00 0502 0000 0100  ........:.......
            0x0030:  8200 b500 2710 0000 0000 0000 0000 0000  ....'...........
            0x0040:  0000 0000 0000 0000 027d 0000            .........}..

我在 tun0 上的应用程序中设置了一个套接字,因为我希望这些是 ICMP 数据包:

int fd = socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6); // ICMP

// ... bind this socket to tun0

  int interfaceIndex = // tun0 interface Index
  int mcastTTL = 10;
  int loopBack = 1;

  if (setsockopt(listener->socket,
                 IPPROTO_IPV6,
                 IPV6_MULTICAST_IF,
                 &interfaceIndex,
                 sizeof(interfaceIndex))
      < 0) {
    perror("setsockopt:: IPV6_MULTICAST_IF:: ");
  }

  if (setsockopt(listener->socket,
                 IPPROTO_IPV6,
                 IPV6_MULTICAST_LOOP,
                 &loopBack,
                 sizeof(loopBack))
      < 0) {
    perror("setsockopt:: IPV6_MULTICAST_LOOP:: ");
  }

  if (setsockopt(listener->socket,
                 IPPROTO_IPV6,
                 IPV6_MULTICAST_HOPS,
                 &mcastTTL,
                 sizeof(mcastTTL))
      < 0) {
    perror("setsockopt:: IPV6_MULTICAST_HOPS::  ");
  }

  struct ipv6_mreq mreq6 = {{{{0}}}};
  MEMCOPY(&mreq6.ipv6mr_multiaddr.s6_addr, sourceAddress, 16);
  mreq6.ipv6mr_interface = interfaceIndex;

  if (setsockopt(listener->socket,
                 IPPROTO_IPV6,
                 IPV6_JOIN_GROUP,
                 &mreq6,
                 sizeof(mreq6))
      < 0) {
    perror("setsockopt:: IPV6_JOIN_GROUP::  ");
  }

以这种方式设置套接字,我可以接收 ICMP 回显请求、回复我自己的地址以及使用 link-local 多播地址发送的多播。但是,我没有看到 任何 MLDv2 查询。

这是我的接收循环:

  uint8_t received[1000] = { 0 };
  struct sockaddr_storage peerAddress = { 0 };
  socklen_t addressLength = sizeof(peerAddress);
  socklen_t addressLength = sizeof(peerAddress);

  int receivedLength = recvfrom(sockfd,
                                received,
                                sizeof(received),
                                0,
                                (struct sockaddr *)&peerAddress,
                                &addressLength);

  if (receivedLength > 0) {
    // Never get here for MLDv2 queries.
  }

进一步研究后,我发现了 IPV6_ROUTER_ALERT 套接字选项,其手册页描述如下:

IPV6_ROUTER_ALERT
Pass forwarded packets containing a router alert hop-by-hop option to this socket.
Only allowed for SOCK_RAW sockets.  The tapped packets are not forwarded by the
kernel, it is the user's responsibility to send them out again.  Argument is a
pointer to an integer.  A positive integer indicates a router alert option value
to intercept.  Packets carrying a router alert option with a value field
containing this integer will be delivered to the socket.  A negative integer
disables delivery of packets with router alert options to this socket.

所以我想我错过了这个选项,并尝试按如下方式设置它。 [RFC2710] 0 表示多播侦听器发现消息。

  int routerAlertOption = 0;

  if (setsockopt(listener->socket,
                 IPPROTO_IPV6,
                 IPV6_ROUTER_ALERT,
                 &routerAlertOption,
                 sizeof(routerAlertOption))
      < 0) {
    perror("setsockopt:: IPV6_ROUTER_ALERT::  ");
  }

但是,这给了我 ENOPROTOOPT 错误(errno 92)。谷歌搜索 (http://www.atm.tut.fi/list-archive/usagi-users-2005/msg00317.html) 使我了解到无法使用 IPPROTO_ICMPV6 协议设置 IPV6_ROUTER_ALERT 选项。它需要一个使用 IPPROTO_RAW 协议定义的套接字。

但是,将我的套接字定义为:

int fd = socket(AF_INET6, SOCK_RAW, IPPROTO_RAW);

意味着我无法再在我的 recvfrom 中接收任何 ICMP 数据包。


TL;DR:如何使用 IPv6 套接字读取 MLDv2 查询?


编辑(答案): Linux 的传统实现似乎在将 MLDv2 数据包传递到 ICMPV6 套接字时会丢弃它们。这是为什么,我不确定。 (可能是因为 next-header 选项。)

我遵循了下面接受的答案,并采用了一种在 tun0 接口上读取原始数据包的方法。我在这里遵循 ping6_ll.c 示例:http://www.pdbuchan.com/rawsock/rawsock.html.

它使用带有 (SOCK_RAW, ETH_P_ALL) 的套接字。您还可以设置一些 SOL_PACKET 选项来过滤接口上的特定多播规则。

从快速浏览 RFC 来看,事情看起来不太好。每 RFC4443 (ICMPv6) 2.4:

2.4. Message Processing Rules

Implementations MUST observe the following rules when processing ICMPv6 messages (from [RFC-1122]):

(b) If an ICMPv6 informational message of unknown type is received, it MUST be silently discarded.

根据 MLDv2 规范,它使用类型 130、143,也许还有其他类型(在 RFC 中没有看到更多图表),而有效的 ICMPv6 类型是 1、2、3、4、101、107、127, 128、129、200、201、255。

如果要将 MLDv2 数据包传递到 ICMPv6 套接字,则实现(内核)似乎必须丢弃它们。就我个人而言,如果常规实现无论如何都会丢弃数据包,那么让 MLDv2 看起来像 ICMPv6 没有多大意义,但我没有看到任何与此声明相矛盾的东西。

您当然可以更深入地使用原始套接字,尤其是考虑到您的堆栈无法识别 MLDv2(也许有一个内核补丁可以解决这个问题?)。但是你必须自己解析 IP 和 ICMP headers。