设置UDP套接字的源IP

时间:2010-06-17 13:53:30

标签: sockets udp interface ip

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

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

有没有办法做到这一点,而无需为每个IP创建一个单独的套接字?

4 个答案:

答案 0 :(得分:26)

Nikolai,使用单独的套接字并为每个地址绑定(2)或弄乱路由表通常不是一个可行的选择,例如动态地址。一个IP_ADDRANY绑定的UDP服务器应该能够看到响应收到数据包的动态分配的IP地址。

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

链接中的代码使用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,可以在传递给ipi_spec_dst的辅助数据中设置struct in_pktinfo的{​​{1}}字段。有关如何在sendmsg(2)中创建和操作辅助数据的指南,请参阅上面引用的帖子cmsg(3)sendmsg(2)。一个例子(这里不保证)可能是:

struct msghdr

请注意,这在IPv6中有所不同:在recvmsg和sendmsg案例中都使用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);

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

(引用的手册页 - 约1个超链接限制)

struct in6_pktinfo::ipi6_addr

答案 1 :(得分:18)

我以为我会扩展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所必需的,因为它可以透明地处理多宿主。

答案 2 :(得分:3)

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

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

答案 3 :(得分:0)

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

我要解决这个问题的方法是

  1. 从收到的数据包中获取接口名称
  2. 将套接字绑定到特定接口
  3. unbind socket
  4. 示例:

      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));