Configuración de la IP de origen para un socket UDP

Tengo un socket UDP que está vinculado a INADDR_ANY para escuchar paquetes en todas las direcciones IP que tiene mi servidor. Estoy enviando respuestas a través del mismo socket.

En este momento, el servidor elige automáticamente qué IP se utiliza como IP de origen cuando se envían los paquetes, pero me gustaría poder configurar la IP de origen saliente.

¿Hay alguna manera de hacerlo sin tener que crear un socket separado para cada IP?

Nikolai, usando un socket separado y bind (2) para cada dirección o jugando con tablas de enrutamiento a menudo no es una opción viable, por ejemplo, con direcciones dinámicas. Un solo servidor IP_ADDRANY UDP debería poder aparecer para responder en la misma dirección IP asignada dinámicamente en la que se recibe un paquete.

Afortunadamente, hay otra manera. Según el soporte de su sistema, puede utilizar las opciones de socket IP_PKTINFO para establecer o recibir datos auxiliares sobre un mensaje. Los datos auxiliares (a través de cmsg(3) ) se cubren en muchos lugares en línea, aunque comp.os.linux.development.system tenía una muestra de código completo específica para IP_PKTINFO .

El código en el enlace usa IP_PKTINFO (o IP_RECVDSTADDR dependiendo de la plataforma) para obtener la dirección de destino de un mensaje UDP a partir de los datos cmsg(3) auxiliares. Parafraseado aquí:

 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, su pregunta preguntó cómo establecer la dirección de origen en los paquetes salientes. Con IP_PKTINFO es posible establecer el campo ipi_spec_dst de la struct in_pktinfo en los datos auxiliares pasados ​​a sendmsg(2) . Consulte la publicación mencionada anteriormente, cmsg(3) y sendmsg(2) para obtener instrucciones sobre cómo crear y manipular los datos auxiliares en una struct msghdr . Un ejemplo (ninguna garantía aquí) podría ser:

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

Tenga en cuenta que esto es diferente en IPv6: use struct in6_pktinfo::ipi6_addr en los casos recvmsg y sendmsg.

Tenga en cuenta también que Windows no admite un equivalente a ipi_spec_dst en la estructura in_pktinfo, por lo que no puede usar este método para establecer la dirección de origen en un paquete de winsock2 saliente.

(páginas del manual a las que se hace referencia: obtener alrededor de 1 límite de hipervínculo)

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

Pensé en expandir Jeremy’s sobre cómo hacer esto para IPv6. Jeremy omite muchos detalles, y cierta documentación (como la página man de Linux para ipv6) es simplemente incorrecta. Primero, en algunas distribuciones debe definir _GNU_SOURCE; de lo contrario, algunas de las cosas de IPv6 no están definidas:

 #define _GNU_SOURCE #include  #include  #include  

Luego configure el socket de una manera bastante estándar que escuche todos los paquetes IP (es decir, tanto IPv4 como IPv6) en un puerto UDP particular:

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

Observe que el código anterior establece las opciones de IP e IPv6 para un socket IPv6. Resulta que si el paquete llega a una dirección IPv4, obtendrá IP_PKTINFO (es decir, IPv4) cmsg a pesar de que es un socket IPv6, y si no los habilita, no se enviarán. También observe que la opción IPV6_RECPKTINFO está configurada (que no se menciona en man 7 ipv6 ), no IPV6_PKTINFO (que se describe erróneamente en man 7 ipv6 ). Ahora recibe un paquete 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); 

El siguiente paso es extraer la interfaz y dirección del paquete UDP que se recibió fuera del 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; } } 

Finalmente, podemos enviar la respuesta de vuelta utilizando el mismo destino.

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

Nuevamente observe cómo, si el paquete entró vía IPv4, tenemos que poner una opción IPv4 en el cmsg aunque sea un socket AF_INET6. Al menos, eso es lo que tienes que hacer para Linux.

Es una cantidad sorprendente de trabajo, pero AFAICT es lo mínimo que tiene que hacer para hacer un servidor UDP robusto que funcione en todos los entornos de Linux concebibles. La mayor parte no es necesaria para TCP porque maneja multihoming de forma transparente.

O bien bind(2) a cada dirección de interfaz y administra múltiples sockets, o deja que el kernel realice la asignación de IP de origen implícita con INADDR_ANY . No hay otra manera.

Mi pregunta sería: ¿por qué necesitas esto? ¿El enrutamiento IP normal no funciona para usted?

Me encontré con el mismo problema recientemente.

Lo que hago para resolver este problema es

  1. obtener el nombre de la interfaz del paquete recibido
  2. unir el socket a una interfaz específica
  3. desconectar el zócalo

Ejemplo:

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