设置UDP套接字的源IP地址
我有一个绑定到INADDR_ANY的UDP套接字来监听我服务器上所有IP地址的数据包。我通过相同的套接字发送回复。设置UDP套接字的源IP地址
现在服务器自动选择使用哪个IP作为发送数据包时的源IP,但我希望能够自己设置传出源IP。
有没有办法做到这一点,而不必为每个IP创建一个单独的套接字?
尼古拉对每个地址使用单独的套接字并绑定(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));
}
}
基因,你的问题问如何设置输出数据包的源地址。使用IP_PKTINFO
可以将struct in_pktinfo
的ipi_spec_dst
字段设置为传递给sendmsg(2)
的辅助数据。请参阅上面提及的帖子,cmsg(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数据包上设置源地址。
(参考手册页 - 让周围1超级链接极限)
http:// linux.die.net/man/2/sendmsg
http:// linux.die.net/man/3/cmsg
您可以为每个接口地址设置bind(2)
并管理多个套接字,也可以让内核执行隐含的源IP分配,其中包括INADDR_ANY
。没有其他办法。
我的问题是 - 你为什么需要这个?普通的IP路由不适合你吗?
我想我会扩大杰里米对如何做到这一点的IPv6。 Jeremy留下了很多细节,并且一些文档(如Linux的ipv6手册页)显然是错误的。首先在一些发行,你必须定义_GNU_SOURCE,否则一些IPv6的东西没有定义:
#define _GNU_SOURCE
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
下一页设立插座监听所有的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地址,您将获得IP_PKTINFO(即IPv4)cmsg,即使它是IPv6套接字,并且如果您不启用它们,它们将不会被发送。另请注意,设置了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);
下一步是提取接口和地址收到的UDP数据包出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;
}
}
最后,我们得到的响应发回,使用相同的目的地。
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是制作健全的UDP服务器所需的最低限度,它可在所有可能的Linux环境中运行。它大部分不是TCP所必需的,因为它透明地处理多重归属。
最近我遇到了同样的问题。
我做些什么来解决这个问题是
- 从接收的数据包获取接口名称
- 绑定套接字到特定的接口
- 解除绑定插座
例子:
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));
我从IBM知识中心找到了关于源IP选择的链接: [https://www.ibm.com/support/knowledgecenter/SSLTBW_2.1.0/com.ibm.zos.v2r1.halz002/tcpip_source_ip_addr_selection.htm]( https://www.ibm.com/support/knowledgecenter/SSLTBW_2.1.0/com.ibm.zos.v2r1.halz002/tcpip_source_ip_addr_selection.htm) – 2016-08-25 10:03:30
谢谢,IP路由工作罚款和数据包到达他们的目的地,但不幸的是,客户都连接到他们的特定服务器IP,协议要求他们从这个特定的IP得到答案。现在所有客户都从同一个IP获得答案。 – 2010-06-17 16:02:57
我的怀疑就是路由表 - 你有单一的默认路由/网关吗?添加特定于客户端地址的路线可能会有帮助 – 2010-06-17 16:18:53
是的,添加主机路线会有所帮助,但我宁愿在我的程序中执行此操作。 – 2010-06-17 17:40:44