Envoi de paquet UDP depuis une interface précise
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.
É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.
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.
L’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
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