设置 UDP 套接字的源 IP

发布于 2024-09-06 04:48:05 字数 167 浏览 3 评论 0原文

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

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

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

I have a UDP socket that is bound to INADDR_ANY to listen to packets on all the IPs my server has. I'm sending out replies through the same socket.

Right now the server chooses automatically which IP is used as the source IP when packets are sent out, but I would like to be able to set the outgoing source IP myself.

Is there any way to do that without having to create a separate socket for each IP ?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

昵称有卵用 2024-09-13 04:48:05

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

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

链接中的代码使用 IP_PKTINFO(或 IP_RECVDSTADDR,具体取决于平台)从辅助 cmsg(3) 数据。这里解释一下:

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

吉恩,你的问题是问如何设置传出数据包的源地址。使用 IP_PKTINFO 可以在传递给 sendmsg(2)。请参阅上面引用的帖子,cmsg(3),和 sendmsg(2) 了解如何创建的指南并操作 struct msghdr 中的 3 个辅助数据。一个示例(此处不保证)可能是:

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

Nikolai, using a separate socket and bind(2) for each address or messing with routing tables is often not a feasible option e.g. with dynamic addresses. A single IP_ADDRANY-bound UDP server should be able to appear to respond on the same dynamically-assigned IP address a packet is received on.

Luckily, there is another way. Depending on your system's support you can make use of the IP_PKTINFO socket options to set or receive ancillary data about a message. Ancillary data (via cmsg(3)) is covered in many places online though comp.os.linux.development.system had a full code sample specific to IP_PKTINFO.

The code in the link uses IP_PKTINFO (or IP_RECVDSTADDR depending on the platform) to get the destination address of a UDP message from the ancillary cmsg(3) data. Paraphrased here:

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, your question asked how to set the source address on outgoing packets. With IP_PKTINFO it is possible to set the ipi_spec_dst field of the struct in_pktinfo in the ancillary data passed to sendmsg(2). See the post referenced above, cmsg(3), and sendmsg(2) for guidelines on how to create and manipulate the 3ancillary data in a struct msghdr. An example (no guarantee here) might be:

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

Note this is different in IPv6: use struct in6_pktinfo::ipi6_addr in both the recvmsg and sendmsg cases.

Note also that Windows does not support an equivalent to ipi_spec_dst in the in_pktinfo struct, so you cannot use this method to set the source address on an outgoing winsock2 packet.

想你只要分分秒秒 2024-09-13 04:48:05

我想我应该扩展 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 不需要其中大部分内容,因为它透明地处理多宿主。

I thought I'd expand Jeremy's on how to do this for IPv6. Jeremy leaves out a lot of detail, and some documentation (like Linux's man page for ipv6) is just plain wrong. First on some distributions you have to define _GNU_SOURCE, otherwise some of the IPv6 stuff isn't defined:

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

Next set up the socket in a fairly standard way that listens for all IP packets (ie, both IPv4 and IPv6) on a particular UDP port:

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

Notice the code above sets both IP and IPv6 options for an IPv6 socket. Turns out if the packet arrives on an IPv4 address, you will get IP_PKTINFO (ie IPv4) cmsg's even though it is an IPv6 socket, and if you don't enable them they won't be sent. Also notice the IPV6_RECPKTINFO option is set (which isn't mentioned in man 7 ipv6), not IPV6_PKTINFO (which is described wrongly in man 7 ipv6). Now receive a udp packet:

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

The next step is to extract the interface and address the UDP packet was received on out of the cmsg:

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

Finally we get to send the response back, using the same destination.

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

Again notice how, if the packet came in via IPv4 we have to put an IPv4 option into the cmsg's even though it is an AF_INET6 socket. At least, that is what you have to do for Linux.

It is a surprising amount of work, but AFAICT it is the minimum you have to do to make a robust UDP server that works in all conceivable Linux environments. Most of it is not required for TCP because it handles multihoming transparently.

沫离伤花 2024-09-13 04:48:05

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

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

You either bind(2) to each interface address and manage multiple sockets, or let the kernel do the implicit source IP assignment with INADDR_ANY. There is no other way.

My question would be - why do you need this? Is normal IP routing not working for you?

过度放纵 2024-09-13 04:48:05

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

我为解决此问题所做的是

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

I encountered the same problem recently.

What I do to solve this problem is

  1. get the interface name from received packet
  2. bind socket to specific interface
  3. unbind socket

Example:

  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));
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文