设置UDP套接字的源IP

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

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

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

尼古拉,使用一个单独的套接字和绑定(2)每个地址或弄乱路由表往往是不可行的select,如dynamic地址。 一个单独的IP_ADDRANY UDP服务器应该能够看起来在接收到数据包的同一个dynamic分配的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 ,可以在传递给sendmsg(2)的辅助数据中设置struct in_pktinfoipi_spec_dst字段。 有关如何创build和操作struct msghdr的辅助数据的指导,请参阅cmsg(3)的文章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

(手册页引用 – 获得1个超链接限制)

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

我想我会扩展杰里米的如何做到这一点的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地址,您将获得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); 

下一步是提取接口并parsingUDP数据包从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套接字。 至less,这是你必须为Linux做的。

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

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

我的问题是 – 为什么你需要这个? 正常的IP路由不适合你吗?

我最近遇到同样的问题。

我做什么来解决这个问题是

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