web-dev-qa-db-fra.com

Définition de l'IP source pour un socket UDP

J'ai un socket UDP lié à INADDR_ANY pour écouter les paquets sur toutes les adresses IP de mon serveur. J'envoie des réponses via le même socket.

À l’heure actuelle, le serveur choisit automatiquement l’adresse IP utilisée comme adresse IP source lors de l’envoi des paquets, mais je souhaite pouvoir définir moi-même l’adresse IP sortante.

Est-il possible de le faire sans avoir à créer un socket séparé pour chaque IP?

26
Gene Vincent

Nikolai, en utilisant un socket séparé et bind (2) pour chaque adresse ou en jouant avec les tables de routage n'est souvent pas une option réalisable, par ex. avec des adresses dynamiques. Un seul serveur UDP lié à IP_ADDRANY devrait pouvoir sembler répondre sur la même adresse IP attribuée dynamiquement qu'un paquet est reçu.

Heureusement, il existe un autre moyen. En fonction de la prise en charge de votre système, vous pouvez utiliser les options de socket IP_PKTINFO pour définir ou recevoir des données auxiliaires sur un message. Les données auxiliaires (via cmsg(3)) sont traitées en ligne à de nombreux endroits, bien que comp.os.linux.development.system dispose d’un échantillon de code complet spécifique à IP_PKTINFO.

Le code dans le lien utilise IP_PKTINFO (ou IP_RECVDSTADDR en fonction de la plate-forme) pour obtenir l'adresse de destination d'un message UDP à partir des données auxiliaires cmsg(3). Paraphrasé ici:

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, votre question demandait comment définir l'adresse source sur les paquets sortants. Avec IP_PKTINFO, il est possible de définir le champ ipi_spec_dst du struct in_pktinfo dans les données auxiliaires transmises à sendmsg(2). Voir le poste référencé ci-dessus, cmsg(3) et sendmsg(2) pour obtenir des instructions sur la création et la manipulation des données auxiliaires dans un struct msghdr. Un exemple (pas de garantie ici) pourrait être:

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

Notez que ceci est différent dans IPv6: utilisez struct in6_pktinfo::ipi6_addr dans les deux cas recvmsg et sendmsg.

Notez également que Windows ne prend pas en charge l'équivalent d'ipi_spec_dst dans la structure in_pktinfo. Vous ne pouvez donc pas utiliser cette méthode pour définir l'adresse source sur un paquet winsock2 sortant.

(pages de manuel référencées - se déplacer dans une limite de lien hypertexte)

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

Je pensais que j'allais expliquer à Jeremy comment faire cela pour IPv6. Jeremy omet beaucoup de détails et une documentation (comme la page de manuel de Linux pour ipv6) est tout simplement fausse. Sur certaines distributions, vous devez d’abord définir _GNU_SOURCE, sinon certains éléments IPv6 ne sont pas définis:

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

Ensuite, configurez le socket d’une manière assez standard qui écoute tous les paquets IP (c’est-à-dire IPv4 et IPv6) sur un port UDP particulier:

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

Notez que le code ci-dessus définit les options IP et IPv6 pour un socket IPv6. Il s'avère que si le paquet arrive sur une adresse IPv4, vous obtiendrez des cmsg IP_PKTINFO (c'est-à-dire IPv4) même s'il s'agit d'un socket IPv6, et si vous ne les activez pas, ils ne seront pas envoyés. Notez également que l'option IPV6_RECPKTINFO est définie (ce qui n'est pas mentionné dans man 7 ipv6 ), et non pas IPV6_PKTINFO (décrite incorrectement dans man 7 ipv6 ). Maintenant, recevez un paquet 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);

L'étape suivante consiste à extraire l'interface et l'adresse à laquelle le paquet UDP a été reçu en dehors du 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;
  }
}

Enfin, nous recevons la réponse en utilisant la même 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);

Encore une fois remarquez comment, si le paquet arrivait via IPv4, nous devons mettre une option IPv4 dans le cmsg même s’il s’agit d’un socket AF_INET6. Au moins, c'est ce que vous devez faire pour Linux.

C'est un travail surprenant, mais AFAICT c'est le minimum que vous devez faire pour créer un serveur UDP robuste qui fonctionne dans tous les environnements Linux imaginables. La plupart de ces informations ne sont pas nécessaires pour TCP, car il gère le multihébergement de manière transparente.

18
Russell Stuart

Vous pouvez soit bind(2) accéder à chaque adresse d’interface et gérer plusieurs sockets, soit laisser le noyau effectuer l’assignation IP source implicite avec INADDR_ANY. Il n'y a pas d'autre moyen.

Ma question serait - pourquoi avez-vous besoin de cela? Le routage IP normal ne fonctionne-t-il pas pour vous?

3
Nikolai Fetissov

J'ai rencontré le même problème récemment.

Ce que je fais pour résoudre ce problème est

  1. récupère le nom d'interface du paquet reçu
  2. lier le socket à une interface spécifique
  3. prise non liée

Exemple:

  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));
0
Chih-Ying Lin