【问题标题】:Setting the source IP for a UDP socket设置 UDP 套接字的源 IP
【发布时间】:2011-03-04 23:34:13
【问题描述】:

我有一个绑定到 INADDR_ANY 的 UDP 套接字来监听我的服务器所有 IP 上的数据包。我正在通过同一个套接字发送回复。

现在服务器在发送数据包时会自动选择哪个IP作为源IP,但我希望能够自己设置传出源IP。

有什么方法可以做到这一点,而不必为每个 IP 创建单独的套接字?

【问题讨论】:

    标签: sockets udp interface ip


    【解决方案1】:

    Nikolai,对每个地址使用单独的套接字和 bind(2) 或弄乱路由表通常不是一个可行的选择,例如带有动态地址。单个IP_ADDRANY-bound UDP 服务器应该能够在接收数据包的同一动态分配的 IP 地址上做出响应。

    幸运的是,还有另一种方法。根据您的系统支持,您可以使用IP_PKTINFO 套接字选项来设置或接收有关消息的辅助数据。尽管comp.os.linux.development.system 有一个特定于IP_PKTINFO 的完整代码示例,但辅助数据(通过cmsg(3))在网上很多地方都有介绍。

    链接中的代码使用IP_PKTINFO(或IP_RECVDSTADDR,取决于平台)从辅助cmsg(3)数据中获取UDP消息的目标地址。此处转述:

    struct msghdr msg;
    struct cmsghdr *cmsg;
    struct in_addr addr;
    // after recvmsg(sd, &msg, flags);
    for(cmsg = CMSG_FIRSTHDR(&msg);
        cmsg != NULL;
        cmsg = CMSG_NXTHDR(&msg, cmsg)) {
      if (cmsg->cmsg_level == IPPROTO_IP && cmsg->cmsg_type == IP_PKTINFO) {
        addr = ((struct in_pktinfo*)CMSG_DATA(cmsg))->ipi_addr;
        printf("message received on address %s\n", inet_ntoa(addr));
      }
    }
    

    Gene,您的问题是如何设置传出数据包的源地址。使用IP_PKTINFO,可以在传递给sendmsg(2) 的辅助数据中设置struct in_pktinfoipi_spec_dst 字段。有关如何在 struct msghdr 中创建和操作辅助数据的指南,请参阅上面引用的帖子 cmsg(3)sendmsg(2)。一个例子(这里不保证)可能是:

    struct msghdr msg;
    struct cmsghdr *cmsg;
    struct in_pktinfo *pktinfo;
    // after initializing msghdr & control data to CMSG_SPACE(sizeof(struct in_pktinfo))
    cmsg = CMSG_FIRSTHDR(&msg);
    cmsg->cmsg_level = IPPROTO_IP;
    cmsg->cmsg_type = IP_PKTINFO;
    cmsg->cmsg_len = CMSG_LEN(sizeof(struct in_pktinfo));
    pktinfo = (struct in_pktinfo*) CMSG_DATA(cmsg);
    pktinfo->ipi_ifindex = src_interface_index;
    pktinfo->ipi_spec_dst = src_addr;
    // bytes_sent = sendmsg(sd, &msg, flags);
    

    注意这在 IPv6 中有所不同:在 recvmsg 和 sendmsg 情况下都使用 struct in6_pktinfo::ipi6_addr

    另请注意,Windows 不支持 in_pktinfo 结构中的 ipi_spec_dst 等效项,因此您不能使用此方法在传出的 winsock2 数据包上设置源地址。

    (参考手册页 - 大约有 1 个超链接限制)

    http:// linux.die.net/man/2/sendmsg
    http:// linux.die.net/man/3/cmsg
    

    【讨论】:

      【解决方案2】:

      我想我会扩展 Jeremy 关于如何为 IPv6 执行此操作的内容。 Jeremy 遗漏了很多细节,一些文档(如 Linux 的 ipv6 手册页)完全是错误的。首先在一些发行版中你必须定义 _GNU_SOURCE,否则一些 IPv6 的东西没有被定义:

      #define _GNU_SOURCE
      #include <netinet/in.h>
      #include <sys/types.h>
      #include <sys/socket.h>
      

      接下来以相当标准的方式设置套接字,以侦听特定 UDP 端口上的所有 IP 数据包(即 IPv4 和 IPv6):

      const int on=1, off=0;
      int result;
      struct sockaddr_in6 sin6;
      int soc;
      
      soc = socket(AF_INET6, SOCK_DGRAM, 0);
      setsockopt(soc, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
      setsockopt(soc, IPPROTO_IP, IP_PKTINFO, &on, sizeof(on));
      setsockopt(soc, IPPROTO_IPV6, IPV6_RECVPKTINFO, &on, sizeof(on));
      setsockopt(soc, IPPROTO_IPV6, IPV6_V6ONLY, &off, sizeof(off));
      memset(&sin6, '\0', sizeof(sin6));
      sin6.sin6_family = htons(AF_INET6);
      sin6.sin6_port = htons(MY_UDP_PORT);
      result = bind(soc, (struct sockaddr*)&sin6, sizeof(sin6));
      

      请注意,上面的代码为 IPv6 套接字设置了 IP 和 IPv6 选项。事实证明,如果数据包到达 IPv4 地址,即使它是 IPv6 套接字,您也会得到 IP_PKTINFO(即 IPv4)cmsg,如果您不启用它们,它们将不会被发送。另请注意,设置了 IPV6_RECPKTINFO 选项(man 7 ipv6 中未提及),而不是 IPV6_PKTINFO(man 7 ipv6 中错误地描述了该选项)。现在收到一个udp数据包:

      int bytes_received;
      struct sockaddr_in6 from;
      struct iovec iovec[1];
      struct msghdr msg;
      char msg_control[1024];
      char udp_packet[1500];
      
      iovec[0].iov_base = udp_packet;
      iovec[0].iov_len = sizeof(udp_packet);
      msg.msg_name = &from;
      msg.msg_namelen = sizeof(from);
      msg.msg_iov = iovec;
      msg.msg_iovlen = sizeof(iovec) / sizeof(*iovec);
      msg.msg_control = msg_control;
      msg.msg_controllen = sizeof(msg_control);
      msg.msg_flags = 0;
      bytes_received = recvmsg(soc, &msg, 0);
      

      下一步是从 cmsg 中提取接收到 UDP 数据包的接口和地址:

      struct in_pktinfo in_pktinfo;
      struct in6_pktinfo in6_pktinfo;
      int have_in_pktinfo = 0;
      int have_in6_pktinfo = 0;
      struct cmsghdr* cmsg;
      
      for (cmsg = CMSG_FIRSTHDR(&msg); cmsg != 0; cmsg = CMSG_NXTHDR(&msg, cmsg))
      {
        if (cmsg->cmsg_level == IPPROTO_IP && cmsg->cmsg_type == IP_PKTINFO)
        {
          in_pktinfo = *(struct in_pktinfo*)CMSG_DATA(cmsg);
          have_in_pktinfo = 1;
        }
        if (cmsg->cmsg_level == IPPROTO_IPV6 && cmsg->cmsg_type == IPV6_PKTINFO)
        {
          in6_pktinfo = *(struct in6_pktinfo*)CMSG_DATA(cmsg);
          have_in6_pktinfo = 1;
        }
      }
      

      最后我们可以使用相同的目的地发回响应。

      int cmsg_space;
      
      iovec[0].iov_base = udp_response;
      iovec[0].iov_len = udp_response_length;
      msg.msg_name = &from;
      msg.msg_namelen = sizeof(from);
      msg.msg_iov = iovec;
      msg.msg_iovlen = sizeof(iovec) / sizeof(*iovec);
      msg.msg_control = msg_control;
      msg.msg_controllen = sizeof(msg_control);
      msg.msg_flags = 0;
      cmsg_space = 0;
      cmsg = CMSG_FIRSTHDR(&msg);
      if (have_in6_pktinfo)
      {
        cmsg->cmsg_level = IPPROTO_IPV6;
        cmsg->cmsg_type = IPV6_PKTINFO;
        cmsg->cmsg_len = CMSG_LEN(sizeof(in6_pktinfo));
        *(struct in6_pktinfo*)CMSG_DATA(cmsg) = in6_pktinfo;
        cmsg_space += CMSG_SPACE(sizeof(in6_pktinfo));
      }
      if (have_in_pktinfo)
      {
        cmsg->cmsg_level = IPPROTO_IP;
        cmsg->cmsg_type = IP_PKTINFO;
        cmsg->cmsg_len = CMSG_LEN(sizeof(in_pktinfo));
        *(struct in_pktinfo*)CMSG_DATA(cmsg) = in_pktinfo;
        cmsg_space += CMSG_SPACE(sizeof(in_pktinfo));
      }
      msg.msg_controllen = cmsg_space;
      ret = sendmsg(soc, &msg, 0);
      

      再次注意,如果数据包通过 IPv4 进入,我们必须将 IPv4 选项放入 cmsg,即使它是 AF_INET6 套接字。至少,这是你必须为 Linux 做的。

      这是一个令人惊讶的工作量,但 AFAICT 是你必须做的最低限度的工作,才能制作一个在所有可以想象的 Linux 环境中工作的强大 UDP 服务器。 TCP 不需要其中的大部分,因为它透明地处理多宿主。

      【讨论】:

        【解决方案3】:

        您可以bind(2) 分配每个接口地址并管理多个套接字,或者让内核使用INADDR_ANY 进行隐式源IP 分配。没有其他办法。

        我的问题是——你为什么需要这个?普通的 IP 路由不适合您吗?

        【讨论】:

        • 谢谢,IP 路由工作正常,数据包到达目的地,但不幸的是,客户端都连接到他们的特定服务器 IP,协议要求他们从这个特定 IP 获得答案。现在所有客户端都从同一个 IP 获得答案。
        • 我怀疑路由表——你有单一的默认路由/网关吗?添加特定于客户端地址的路由可能会有所帮助。
        • 是的,添加主机路由会有所帮助,但我更愿意在我的程序中这样做。
        【解决方案4】:

        我最近遇到了同样的问题。

        我解决这个问题的方法是

        1. 从收到的数据包中获取接口名称
        2. 将套接字绑定到特定接口
        3. 解除绑定套接字

        例子:

          struct ifreq ifr;
          ...
          recvmsg(fd, &msg...)
          ...      
          if (msg.msg_controllen >= sizeof(struct cmsghdr))
            for (cmptr = CMSG_FIRSTHDR(&msg); cmptr; cmptr = CMSG_NXTHDR(&msg, cmptr))
              if (cmptr->cmsg_level == SOL_IP && cmptr->cmsg_type == IP_PKTINFO)
              {
                iface_index = ((struct in_pktinfo *)CMSG_DATA(cmptr))->ipi_ifindex;
              }
          if_indextoname(iface_index , ifr.ifr_name);
          mret=setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, &ifr, sizeof(ifr));
        
          sendmsg(...);
        
          memset(&ifr, 0, sizeof(ifr));
          snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "");
          mret=setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, &ifr, sizeof(ifr));
        

        【讨论】:

        猜你喜欢
        • 2012-11-12
        • 2010-10-15
        • 2012-04-10
        • 1970-01-01
        • 2014-07-15
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多