Envoi de paquet UDP depuis une interface précise

C


Cet article fait suite à une déconvenue rencontrée sur un serveur UDP possédant plusieurs interfaces sur le même réseau. Tant que celui-ci écoutait sur une seul et unique interface tout fonctionnait bien mais dés que l’on le configurait pour écouter toutes les interfaces, le client ne recevait pas de réponse du serveur. Pour analyser ce problème, j’ai isolé la partie de communication UDP, reproduis le contexte d’utilisation du serveur puis exécuter de multiples tests de fonctionnement pour comprendre ce qu’il se passait.

Le contexte

Notre serveur possède 2 IP publics, 1 est utilisée par notre serveur UDP et la seconde est utilisée par d’autres services et est accessoirement la route principale de notre serveur. Nous ne pouvons pas modifier la configuration réseau de celui-ci ni même modifier les routes configurées. Le serveur UDP écoute sur le port 50000 et renvoi au client l’écho de la commande envoyée.

Nous simulerons le serveur avec une machine virtuelle VirtualBox possédant 1 interface physique (192.168.56.101) configurée en route principale et 1 alias sur le même réseau (192.168.56.10). La configuration faite dans le fichier /etc/network/interface est la suivante:

auto lo
iface lo inet loopback

auto eth0
iface eth0 inet static
        address 192.168.56.101
        netmask 255.255.255.0
        gateway 192.168.56.1

auto eth0:0
iface eth0:0 inet static
        address 192.168.56.10
        netmask 255.255.255.255

Le client sera simulé sur l’hôte de la machine virtuelle (192.168.56.1) par 2 instances du programme netcat, 1 instance sur l’IP principale et 1 sur l’alias.

$ netcat -vv -n -u 192.168.56.101 50000
$ netcat -vv -n -u 192.168.56.10 50000

Analyse du problème

Avant l’analyse du problème, j’ai reconstitué la partie communication du serveur de manière simplifiée. Vous trouverez le code dans mon dépôt Github udp-echo dans le fichier /src/udpbase.c.

Au début du code, nous définissons le port dans le define PORT et l’adresse d’écoute du serveur via le define LISTEN. Nous utiliserons ce dernier define pour définir si notre serveur écoutera sur 1 IP précise ou sur toutes les interfaces. Commençons les tests.

Écoute sur une interface

Pour commencer les tests nous écouterons uniquement sur l’alias 192.168.56.10. Pour cela nous définissons LISTEN sur cette interface.

#define LISTEN "192.168.56.10"

Nous compilons le serveur et l’exécutons

$ gcc server_base.c && ./a.out

Coté client, nous envoyons un message et recevons bien une réponse

$ ncat -vv 192.168.56.10 50000 -u -n
(UNKNOWN) [192.168.56.10] 50000 (?) open
test
test

Coté serveur, nous voyons que les données sont reçu avec comme source 192.168.56.1 et qu’une réponse est envoyé.

5 bytes received
From:    192.168.56.1:60898
Data:    test

5 bytes sent

Wireshark, nous confirme le bon fonctionnement du serveur.
capture Wireshark montrant le bon fonctionnement du serveur

Écoute sur toutes les interfaces

Pour écouter sur toutes les interfaces, il suffit de changer le #define LISTEN "192.168.56.10" par #define LISTEN "*", de compiler le code et de l’exécuter.

Testons maintenant la communication. Coté client, nous envoyons un message mais aucune réponse n’arrive.

$ ncat -vv 192.168.56.10 50000 -u -n
(UNKNOWN) [192.168.56.10] 50000 (?) open
test

Pourtant le serveur nous dit bien avoir envoyé les données.

5 bytes received
From:    192.168.56.1:60898
Data:    test

5 bytes sent

Le scan via Wireshark (ou tcpdump) nous confirme que le serveur envoie bien une réponse.
Screenshot Wireshark
Cependant, la réponse a la bonne destination mais pas la bonne source. Ce qui explique la non réception du message coté client, le client ne faisant pas le lien entre les 2 IPs.

Mais alors pourquoi ça ne fonctionne pas ?

C’est ce que nous allons chercher à comprendre.

Extrait du modèle OSIL’UDP est un protocole de niveau 4 (transport) du modèle OSI. Ce protocole est non connecté, ne garantie pas la délivrabilité du message ni l’ordre d’arrivée de celle-ci. Bon OK et alors ? Le header UDP définit le port source et le port de sortie du message mais pas l’IP source ni l’IP de destination, qui sont elles, définies au niveau inférieur la couche IP (niveau 3).

Lorsque nous lions le socket sur une seule et unique interface, il répond seulement par cette interface, c’est d’ailleurs un moyen pour forcer l’interface utilisée lors de l’envoi de message. Mais lorsque le socket écoute sur toutes les interfaces, il répond en disant « envoi ce paquet à cette destination via une des interfaces écoutées ».

Le programme utilise les fonctions recvfrom et sendto. Les prototypes de ces fonctions sont les suivants :

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

Le prototype de sendto ne permet pas de spécifier l’interface de sortie mais juste le destinataire. Il nous faut donc utilisé une autre fonction.

Cherchons une solution

Dans le man sendto, il existe une fonction sendmsg dont le prototype est le suivant :

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

Le prototype est plus succinct et contient une nouvelle structure msghdr (Message Header).

struct msghdr {
    void         *msg_name;       /* adresse de destination */
    socklen_t     msg_namelen;    /* sizeof msg_name */
    struct iovec *msg_iov;        /* Tableau de buffer */
    size_t        msg_iovlen;     /* # elements dans msg_iov */
    void         *msg_control;    /* Informations complémentaires */
    size_t        msg_controllen; /* sizeof msg_control */
    int           msg_flags;      /* flags on received message */
};

Le man nous dit que les informations complémentaires sont accessibles uniquement via les macros définies dans cmsg(3). Cette nouvelle page, nous dit que les informations contenues dans les informations complémentaires voulues doivent être définies dans les options du socket. Les options de sockets dédié à la couche IP sont visibles dans le man ip(7). Parmi ces options, nous utiliserons IP_PKTINFO qui permet de recevoir et transmettre l’interface utilisée par la socket. La page de man nous précise que les données contenues dans msg_control seront de type struct pktinfo définit comme ci-dessous :

struct in_pktinfo {
    unsigned int   ipi_ifindex;   /* Numéro d'interface     */
    struct in_addr ipi_spec_dst;  /* Adresse locale         */
    struct in_addr ipi_addr;      /* Adresse de destination */
};

Comme vous pouvez le voir, cette structure nous permet de définir l’interface utilisée, nous avons donc trouvé notre fonction et l’option devant être utilisée par le socket.

Pour spécifier cette options nous utilisons la fonction setsockopt.

setsockopt(sockfd, IPPROTO_IP, IP_PKTINFO, &sockopt, sizeof sockopt);

Le final

Après avoir suivi l’ensemble des pages de man et fait de multiples essais, nous arrivons au résultat visible dans le fichier udpfinal.c qui après compilation nous permet d’écouter sur toutes les interfaces, de recevoir un message et de le réémettre via la même interface.

Wireshark est toujours la pour nous confirmer la bonne correction
Screenshot Wireshark

Problème résolu. Si vous avez d’autres astuces ou solutions pour régler ce problème, partagez les en commentaire.

Les autres solutions envisagées ont été :

  • Création d’un socket par interface
  • Création d’un nouveau socket pour la réponse utilisant l’option SO_BINDTODEVICE


Sources utilisés :

https://groups.google.com/forum/#!original/comp.os.linux.development.system/7Eql8Xkef7o/4W9-jCecneAJ
http://stackoverflow.com/questions/3062205/setting-the-source-ip-for-a-udp-socket

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.