设置UDP套接字的源IP

问题描述 投票:0回答:4

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

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

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

sockets udp interface ip
4个回答
33
投票

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
sendmsg(2)
字段。有关如何在 cmsg(3)
 中创建和操作 3 个辅助数据的指南,请参阅上面引用的帖子 
sendmsg(2)
struct msghdr
。一个例子(这里不保证)可能是:

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数据包上设置源地址。


21
投票

我想我应该扩展 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 做的事情。

这是一个令人惊讶的工作量,但据我所知,这是制作一个可以在所有可以想象的 Linux 环境中工作的强大 UDP 服务器所需要做的最少工作。 TCP 不需要其中大部分内容,因为它透明地处理多宿主。


4
投票

您可以

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

我的问题是 - 为什么你需要这个?普通的 IP 路由对您不起作用吗?


0
投票

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

我为解决这个问题所做的是

  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));
© www.soinside.com 2019 - 2024. All rights reserved.