Linux Embedded

Le blog des technologies libres et embarquées

Premiers pas avec la stack lwIP

Présentation générale

lwIP (Lightweight IP) est une pile logicielle qui implémente une grande partie de la suite de protocoles TCP/IP: Ethernet, ARP, DHCP, IPV4, IPV6, UDP, TCP, DNS, HTTP, PPP, etc…).

Comme son nom l’indique, la stack lwIP se veut légère et est donc conçue de manière à minimiser son empreinte mémoire et sa consommation CPU. L'empreinte mémoire dépend bien entendue de la cible matérielle, du compilateur, et des options activées dans la stack mais il est possible par exemple de descendre sous les 40ko de ROM et une dizaine de ko de RAM. Codée en C, configurable et ne faisant appel qu’à un nombre limité de dépendances externes, son intégration dans un projet logiciel reste simple et ses cibles principales sont les systèmes à microcontrôleurs en bare metal ou exécutant des “petits” OS temps réel (RTOS).

C’est un projet open source initialement développé par Adam Dunkels et désormais maintenu par une large communauté. Le projet est hébergé ici https://savannah.nongnu.org/projects/lwip/ où on trouve, entre autres, le dépôt git https://git.savannah.nongnu.org/cgit/lwip.git, de la documentation https://www.nongnu.org/lwip/2_1_x/index.html et des listes de discussion https://savannah.nongnu.org/mail/?group=lwip qui permettent d’obtenir du support de la communauté.

La stack lwIP est aujourd’hui utilisée avec succès dans de nombreux projets industriels et est intégrée nativement dans plusieurs environnements de développement comme par exemple celui fourni par ST pour sa famille de microcontrôleurs STM32, l’environnement Arduino ou même celui d’Espressif pour la famille des ESP32.

Ce premier article a pour objectif de présenter l’architecture logicielle et les principaux concepts techniques de lwIP. Pour cela nous allons jouer avec la stack non pas sur un microcontrôleur, mais sur un système Linux dans l’unique but de faciliter nos premières expérimentations.

Note: Dans de prochains articles nous irons vers une utilisation plus concrète de lwIP et nous pourrons voir comment la porter sur un microcontrôleur en bare metal ou avec un RTOS, comment intégrer le support de TLS pour des communications sécurisées, comment connecter un modem cellulaire pour rendre notre système “sans fil”, … bref tout ce dont vous avez besoin pour votre projet IoT et le développement d’un objet connecté “léger et autonome” capable de fonctionner sur batterie plusieurs années si nécessaire.

Exploration du dépôt et des sources

Commençons par cloner le dépôt:

remy@remy-VirtualBox:~$ mkdir test-lwip
remy@remy-VirtualBox:~$ cd test-lwip/
remy@remy-VirtualBox:~/test-lwip$ git clone https://git.savannah.gnu.org/git/lwip.git
Cloning into 'lwip'...
remote: Counting objects: 55161, done.
remote: Compressing objects: 100% (12820/12820), done.
remote: Total 55161 (delta 41660), reused 55161 (delta 41660)
Receiving objects: 100% (55161/55161), 10.59 MiB | 951.00 KiB/s, done.
Resolving deltas: 100% (41660/41660), done.
remy@remy-VirtualBox:~/test-lwip$ cd lwip/
remy@remy-VirtualBox:~/test-lwip/lwip$ git status
On branch master
Your branch is up to date with 'origin/master'.

nothing to commit, working tree clean
Entrons dans le répertoire de notre clone puis vérifions sur quel commit nous nous trouvons:  
remy@remy-VirtualBox:~/test-lwip$ cd lwip
remy@remy-VirtualBox:~/test-lwip/lwip$ git log
commit 34e435c78611fbf21c49c5ddb6c395a097e24cc7 (HEAD -> master, origin/master, origin/HEAD)
Author: Florian La Roche <florian.laroche@gmail.com>
Date:   Mon Nov 22 08:34:22 2021 +0100

    tapif: fix strncpy
    
    Adjust the strncpy() call in tapif.c to how this is coded
    in other strncpy() invocations. Also gcc warns about this.
    
    See patch #10142

Voyons les différentes branches disponibles:

remy@remy-VirtualBox:~/test-lwip/lwip$ git branch -r
  origin/DEVEL-1_4_1
  origin/HEAD -> origin/master
  origin/STABLE-2_0_0
  origin/STABLE-2_1_x
  origin/master

Les branches DEVEL-1_4_1 et STABLE-2_0_0 correspondent à d’anciennes versions qui ne sont plus maintenues depuis respectivement 2012 et 2017.

La branche STABLE-2_1_X est la branche active en cours de maintenance. Le tag le plus récent est STABLE-2_1_3_RELEASE qui correspond à la release v2.1.3 dont la publication date de novembre 2021 : https://savannah.nongnu.org/forum/forum.php?forum_id=10072

Enfin la branche master est la branche de développement : la plus à jour mais aussi parfois instable.

Pour la suite de nos expérimentations nous resterons sur la branche master.

Explorons maintenant l’arborescence du dépôt de sources :

remy@remy-VirtualBox:~/test-lwip/lwip$ tree -L 2
.
├── BUILDING
├── CHANGELOG
├── CMakeLists.txt
├── codespell_changed_files.sh
├── codespell_check.sh
├── contrib
│   ├── addons
│   ├── apps
│   ├── Coverity
│   ├── examples
│   ├── Filelists.cmake
│   ├── Filelists.mk
│   └── ports
├── COPYING
├── doc
│   ├── contrib.txt
│   ├── doxygen
│   ├── FILES
│   ├── mdns.txt
│   ├── mqtt_client.txt
│   ├── NO_SYS_SampleCode.c
│   ├── ppp.txt
│   ├── savannah.txt
│   └── ZeroCopyRx.c
├── FEATURES
├── FILES
├── README
├── src
│   ├── api
│   ├── apps
│   ├── core
│   ├── Filelists.cmake
│   ├── Filelists.mk
│   ├── FILES
│   ├── include
│   └── netif
├── test
│   ├── fuzz
│   ├── sockets
│   └── unit
└── UPGRADING

18 directories, 23 files

Dans le répertoire doc/ on trouve toute la configuration doxygen qui permet de générer la documentation que l’on retrouve ici : https://www.nongnu.org/lwip/2_1_x/index.html

Dans le répertoire src/include/ on trouve naturellement les headers des différents modules qui composent la pile logicielle.

Dans le répertoire src/netif/ on trouve l’implémentation de la couche liaisons de données. Bien entendu Ethernet mais aussi par exemple PPP (utile à la connexion avec un modem), SLIP (liaison série) ou 6LoWPAN (liaison radio) sont supportés.

Dans le répertoire src/core/ on trouve l’implémentation des couches réseau et transport (ARP, IPV4, IPV6, UDP, TCP, etc) mais aussi l’implémentation des mécanismes internes nécessaires au fonctionnement de la stack comme par exemple la gestion des buffers qui transportent les trames réseaux au travers des différentes couches de protocoles (pbuf.c) la gestion d’un pool de mémoire (memp.c) ou d’un tas (mem.c), la gestion de timeouts (timeouts.c), etc.

Dans le répertoire src/apps/ on trouve l’implémentation de la couche application: HTTP, MQTT, SNTP, etc.

Dans le répertoire src/api/ on trouve différents types d’interfaces, de plus ou moins “haut niveau”, qui permettent depuis votre application d’interagir plus facilement avec lwIP mais qui nécessitent les services d’un RTOS (préemption, mutex, …). Voir le chapitre sur Les différents types d’API

Dans le répertoire contrib/ on trouve divers projets exemples de l’utilisation de la stack lwIP (contrib/apps/ et contrib/examples/) mais aussi des exemples de portage dans divers environnement (contrib/ports/).

Note: Ces contributions étaient initialement externes au projet lwIP et donc dans un dépôt à part (https://git.savannah.nongnu.org/cgit/lwip/lwip-contrib.git). Mais ce dépôt est maintenant obsolète puisque son contenu a été fusionné dans le dépôt principal.

Le portage sous Unix

Comme évoqué précédemment, pour des raisons de simplicité, nous allons commencer par manipuler la stack lwIP dans un environnement Linux donc Unix.

Tous les fichiers nécessaires au portage de la stack dans cet environnement sont présents dans contrib/ports/unix/.

Le portage de la stack sur Linux n’est pas conçu pour remplacer la pile réseau qui s’exécute dans le kernel. Au contraire, il exécute la stack lwIP dans l’espace utilisateur en la connectant à la pile réseau du kernel via une interface de type tap (https://debian-facile.org/doc:reseau:interfaces:tapbridge).

Les deux piles s’exécutent donc en parallèle mais dans deux espaces distincts.

Voir le fichier contrib/ports/unix/port/netif/tapif.c pour comprendre l’utilisation de l’interface tap côté lwIP.

Voir aussi le script contrib/ports/unix/setup-tapif qui permet de créer et de monter l’interface tap nécessaire.

Ce portage sous Unix a été créé pour permettre d’exécuter les tests unitaires de la stack mais il peut surtout vous servir à développer, maquetter ou simuler votre application lwIP sur votre distribution Linux préférée avant son portage sur une cible réelle.

Note: Au delà de l’utilisation d’une interface type tap, le portage sous Unix peut aussi fonctionner via une interface pcap (“packet capture” qui permet de rejouer une trace réseau depuis un fichier) ou une interface série. Voir contrib/ports/unix/README.

Compilation et exécution d’un premier exemple (serveur HTTP)

La stack lwIP fournit tout le support pour compiler votre projet avec cmake. Voir entre autres les différents fichiers Filelists.cmake présents dans les différents répertoires listant les fichiers à compiler mais aussi le fichier CMakeLists.txt qui permet de compiler les exemples dans notre environnement Linux.

Essayons de lancer une première compilation avec les commandes cmake habituelles :

remy@remy-VirtualBox:~/test-lwip/lwip$ mkdir build
remy@remy-VirtualBox:~/test-lwip/lwip$ cd build/
remy@remy-VirtualBox:~/test-lwip/lwip/build$ cmake ..
-- The C compiler identification is GNU 9.3.0
-- The CXX compiler identification is GNU 9.3.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- CMAKE_BUILD_TYPE not set - defaulting to Debug build.
-- Build type: Debug
-- LWIP_MBEDTLSDIR not set - using default location /home/remy/test-lwip/lwip/../mbedtls
-- Could NOT find Doxygen (missing: DOXYGEN_EXECUTABLE) 
-- Doxygen needs to be installed to generate the doxygen documentation
-- Configuring done
-- Generating done
-- Build files have been written to: /home/remy/test-lwip/lwip/build
remy@remy-VirtualBox:~/test-lwip/lwip/build$ make
Scanning dependencies of target makefsdata
[  1%] Building C object contrib/ports/unix/example_app/CMakeFiles/makefsdata.dir/__/__/__/__/src/apps/http/makefsdata/makefsdata.c.o
[  2%] Linking C executable makefsdata
[  2%] Built target makefsdata
Scanning dependencies of target lwipcontribexamples
[  2%] Building C object contrib/ports/unix/example_app/CMakeFiles/lwipcontribexamples.dir/__/__/__/examples/httpd/fs_example/fs_example.c.o
[  3%] Building C object contrib/ports/unix/example_app/CMakeFiles/lwipcontribexamples.dir/__/__/__/examples/httpd/https_example/https_example.c.o
[  3%] Building C object contrib/ports/unix/example_app/CMakeFiles/lwipcontribexamples.dir/__/__/__/examples/httpd/ssi_example/ssi_example.c.o
[  4%] Building C object contrib/ports/unix/example_app/CMakeFiles/lwipcontribexamples.dir/__/__/__/examples/lwiperf/lwiperf_example.c.o

...

[ 95%] Building C object contrib/ports/unix/example_app/CMakeFiles/lwipallapps.dir/__/__/__/__/src/apps/mdns/mdns.c.o
[ 96%] Building C object contrib/ports/unix/example_app/CMakeFiles/lwipallapps.dir/__/__/__/__/src/apps/mdns/mdns_out.c.o
[ 96%] Building C object contrib/ports/unix/example_app/CMakeFiles/lwipallapps.dir/__/__/__/__/src/apps/mdns/mdns_domain.c.o
[ 97%] Building C object contrib/ports/unix/example_app/CMakeFiles/lwipallapps.dir/__/__/__/__/src/apps/netbiosns/netbiosns.c.o
[ 97%] Building C object contrib/ports/unix/example_app/CMakeFiles/lwipallapps.dir/__/__/__/__/src/apps/tftp/tftp.c.o
[ 98%] Building C object contrib/ports/unix/example_app/CMakeFiles/lwipallapps.dir/__/__/__/__/src/apps/mqtt/mqtt.c.o
[ 99%] Linking C static library liblwipallapps.a
[ 99%] Built target lwipallapps
Scanning dependencies of target example_app
[ 99%] Building C object contrib/ports/unix/example_app/CMakeFiles/example_app.dir/__/__/__/examples/example_app/test.c.o
/home/remy/test-lwip/lwip/contrib/examples/example_app/test.c:110:10: fatal error: lwipcfg.h: No such file or directory
  110 | #include "lwipcfg.h"
      |          ^~~~~~~~~~~
compilation terminated.
make[2]: *** [contrib/ports/unix/example_app/CMakeFiles/example_app.dir/build.make:63: contrib/ports/unix/example_app/CMakeFiles/example_app.dir/__/__/__/examples/example_app/test.c.o] Error 1
make[1]: *** [CMakeFiles/Makefile2:164: contrib/ports/unix/example_app/CMakeFiles/example_app.dir/all] Error 2
make: *** [Makefile:106: all] Error 2

On voit ici une erreur: il manque le fichier lwipcfg.h. Ce fichier est prévu pour permettre de choisir et de configurer l’exemple à compiler. Heureusement un modèle de ce fichier est disponible ici contribs/examples/example_app/lwipcfg.h.example.

Commençons par le copier et le renommer correctement :

remy@remy-VirtualBox:~/test-lwip/lwip/build$ cp ../contrib/examples/example_app/lwipcfg.h.example ../contrib/examples/example_app/lwipcfg.h

Puis on adapte les différents #define qu’il contient afin de ne compiler que l’exemple du serveur HTTP avec une IP statique :

#define USE_DHCP         0
#define USE_AUTOIP       0
#define LWIP_HTTPD_APP   1

Enfin on relance la compilation :

remy@remy-VirtualBox:~/test-lwip/lwip/build$ make
[  2%] Built target makefsdata
[ 10%] Built target lwipcontribexamples
[ 14%] Built target lwipcontribportunix
[ 15%] Built target lwipcontribaddons
[ 23%] Built target lwipcontribapps
[ 75%] Built target lwipcore
[ 77%] Built target lwipmbedtls
[ 99%] Built target lwipallapps
[ 99%] Building C object contrib/ports/unix/example_app/CMakeFiles/example_app.dir/__/__/__/examples/example_app/test.c.o
[100%] Building C object contrib/ports/unix/example_app/CMakeFiles/example_app.dir/default_netif.c.o
[100%] Linking C executable example_app
[100%] Built target example_app

La compilation ayant réussi, on peut essayer de démarrer notre exécutable :

remy@remy-VirtualBox:~/test-lwip/lwip/build$ ./contrib/ports/unix/example_app/example_app 
Starting lwIP, local interface IP is dhcp-enabled
tapif_init: /dev/net/tun ioctl TUNSETIFF: Operation not permitted

Si vous obtenez l’erreur ci-dessus c’est que vous avez oublié de créer et de monter votre interface tap via le script fourni :

remy@remy-VirtualBox:~/test-lwip/lwip/build$ source ../contrib/ports/unix/setup-tapif 

On peut vérifier avec la commande ip que l’interface (tap0) et le bridge (lwipbridge) correspondant sont bien créés et “up” :

remy@remy-VirtualBox:~/test-lwip/lwip/build$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 08:00:27:a9:61:1e brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic noprefixroute enp0s3
       valid_lft 67208sec preferred_lft 67208sec
    inet6 fe80::d769:3fd6:b0a4:d9ea/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:29:9c:67:b8 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
4: tap0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc fq_codel master lwipbridge state DOWN group default qlen 1000
    link/ether be:74:95:c5:4b:fe brd ff:ff:ff:ff:ff:ff
    inet6 fe80::bc74:95ff:fec5:4bfe/64 scope link 
       valid_lft forever preferred_lft forever
5: lwipbridge: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default qlen 1000
    link/ether be:74:95:c5:4b:fe brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.1/24 scope global lwipbridge
       valid_lft forever preferred_lft forever
    inet6 fe80::bc74:95ff:fec5:4bfe/64 scope link 
       valid_lft forever preferred_lft forever

On relance notre exécutable :

remy@remy-VirtualBox:~/test-lwip/lwip/build$ ./contrib/ports/unix/example_app/example_app 
Starting lwIP, local interface IP is 192.168.1.200
SIOCSIFADDR: Operation not permitted
SIOCSIFFLAGS: Operation not permitted
SIOCSIFNETMASK: Operation not permitted
ifconfig returned 65280
ip6 linklocal address: FE80::12:34FF:FE56:78AB
status_callback==UP, local interface IP is 192.168.1.200
status_callback==UP, local interface IP is 192.168.1.200

Puis avec votre navigateur préféré vous pouvez vous connecter sur l’IP indiquée (ici 192.168.1.200) pour accéder à la page HTML statique qui est servie par notre application :

Le code source de cette page HTML est situé dans le répertoire src/apps/http/fs. Si vous modifiez, par exemple, le fichier src/apps/http/fs/index.html et que vous relancez votre application, vous verrez que la page HTML n’est pas modifiée et affiche toujours l’ancienne version…

La raison à cela est que lwIP étant prévue pour adresser des systèmes à microcontrôleurs qui ne supportent pas toujours un filesystem, le contenu du répertoire est tout d’abord converti, via l’utilitaire src/apps/http/makefsdata, en tableaux binaires dans les fichiers sources lwip/src/apps/http/fsdata.c et fsdata.h qui sont finalement compilés et linkés statiquement dans l'exécutable final.

Après avoir modifié le contenu du répertoire stc/apps/http/fs il convient donc d’exécuter makefsdata avant de recompiler le tout :

remy@remy-VirtualBox:~/test-lwip/lwip/build$ ./contrib/ports/unix/example_app/makefsdata ../src/apps/http/fs

 makefsdata v2.2.0d - HTML to C source converter
     by Jim Pettinato               - circa 2003 
     extended by Simon Goldschmidt  - 2009 

HTTP 1.0 header will be statically included.
  Processing all files in directory ../src/apps/http/fs and subdirectories...

processing subdirectory /img/...
processing /img/280px-Logo_Smile.png...
processing /img/sics.gif...
processing /404.html...
processing /index.html...

Creating target file...


Processed 4 files - done.

remy@remy-VirtualBox:~/test-lwip/lwip/build$ mv fsdata.c ../src/apps/http/fsdata.c
remy@remy-VirtualBox:~/test-lwip/lwip/build$ make clean && make

Puis on relance notre application et on se reconnecte à notre serveur pour voir apparaître notre page web modifiée :

Création pas à pas de notre propre exemple (serveur UDP)

Nous avons vu dans le chapitre précédent comment compiler et exécuter un des nombreux exemples fournis avec la stack lwIP. Nous allons maintenant réaliser pas à pas les différentes étapes qui permettent de créer notre propre exemple afin de mieux comprendre comment configurer la stack, utiliser ses apis et compiler un projet complet avec cmake. L’exemple choisi est un serveur UDP qui écoute sur le port 60001 et répond à toute sollicitation par “Bien reçu ! :” suivi d’une recopie du message reçu.

Les différents styles d’API (raw, netconn, socket)

La stack lwIP met à disposition plusieurs styles d’API pour la programmation de votre application :

  • l’API “raw” aussi appelée “callback style” est la plus légère. Aucune de ses fonctions n’est bloquante car le principe est de configurer des callbacks vers l’application qui seront appelées depuis la stack sur des événements particuliers. Par exemple, la fonction udp_recv() ne fait que configurer la callback qui sera exécutée, plus tard, à chaque réception d’un paquet UDP. Attention : l’api “raw” n’est pas “thread safe” ! Elle ne doit donc être utilisée que dans un environnement “mono-thread” ou ne doit être appelée que depuis le thread qui exécute la stack.
  • les API “netconn” et “socket” sont de type “sequential style” et sont donc bloquantes. Par exemple, la fonction netconn_recv() bloque le thread qui l’exécute jusqu’à ce qu’un paquet soit reçu. Ces APIs nécessitent le support d’un RTOS (préemption, mutex, sémaphore, …) pour fonctionner.

Pour notre exemple nous utiliserons l’API “raw”.

La configuration de la stack

La stack est configurable au travers de divers #define qui permettent d’activer/désactiver certaines fonctionnalités ou de régler la taille de certains buffers afin de vous permettre d’optimiser l’empreinte mémoire mais aussi de modifier le comportement fonctionnel de la stack afin de l'adapter au besoin de votre application.

La liste des #define disponibles ainsi que leur valeur par défaut sont visibles dans le fichier lwip/src/include/lwip/opt.h.

Pour adapter la valeur de ces #define au besoin de notre application il ne faut pas modifier directement le fichier lwip/src/include/lwip/opt.h mais ajouter dans notre projet un fichier lwipopts.h qui nous permettra de “surcharger” la valeur des #define qui nous intéressent. La présence de ce fichier lwipopts.h est obligatoire car il est automatiquement inclus depuis le fichier lwip/src/include/lwip/opt.h:

/*
 * Include user defined options first. Anything not defined in these files
 * will be set to standard values. Override anything you don't like!
 */
#include "lwipopts.h"

Voici les options que nous modifions pour notre exemple d’application:

  • NO_SYS à 1 afin d’indiquer à la stack que nous travaillons sans le support d’un RTOS, donc dans un environnement mono-thread.
  • LWIP_SOCKET et LWIP_NETCONN à 0 car nous n’utiliserons pas les API “socket” et “netconn” puisque comme expliqué plus haut ces APIs nécessitent le support d’un RTOS.
  • LWIP_NETIF_STATUS_CALLBACK et LWIP_NETIF_LINK_CALLBACK à 1 afin que notre application soit avertie des changements d’état des interfaces réseaux au travers de fonctions de callback.

Remarque : Le support de l’UDP, nécessaire pour notre exemple, est déjà activé par les valeurs par défaut du fichier lwip/src/include/lwip/opt.h nous n’avons donc pas besoin de l’activer explicitement dans notre fichier de configuration lwipopts.h

Ce qui nous donne le fichier lwipopts.h suivant: 

#ifndef LWIP_LWIPOPTS_H
#define LWIP_LWIPOPTS_H

#define NO_SYS       1
#define LWIP_SOCKET  0
#define LWIP_NETCONN 0

#define LWIP_NETIF_STATUS_CALLBACK  1
#define LWIP_NETIF_LINK_CALLBACK    1

#endif /* LWIP_LWIPOPTS_H */

Initialisation de la stack

Dans notre fichier my-lwip-app.c, il convient tout d’abord d’appeler la fonction lwip_init() qui se charge d’initialiser tous les modules de la stack conformément aux options de support activées par nos fichiers de configuration :

#include <stdio.h>
#include "lwip/init.h"

int main(int argc, char * argv)
{
   printf("Hello World!\n");

   /* Init lwip stack */
   lwip_init();

Attention: on utilise ici la fonction lwip_init() car NO_SYS=1 mais dans le cas contraire il faut utiliser tcpip_init().

Ajout d’une interface réseau

Ensuite on vient ajouter une interface réseau :

  • à laquelle on assigne l’IP 192.168.1.200
  • qu’on associe avec l’implémentation tapif du portage unix en lui passant un pointeur vers la fonction tapif_init()
  • à laquelle on indique que le point d’entrée permettant d’injecter dans la stack les paquets reçus est netif_input()
  • et qu’on définit comme étant l’interface par défaut
  #include "lwip/netif.h"
  #include "netif/tapif.h"

   struct netif my_netif;
   ip4_addr_t my_ipaddr;
   ip4_addr_t my_netmask;
   ip4_addr_t my_gw;

   ip4_addr_set_zero(&my_ipaddr);
   ip4_addr_set_zero(&my_netmask);
   ip4_addr_set_zero(&my_gw);
   IP4_ADDR((&my_ipaddr),   192,168,  1,200);
   IP4_ADDR((&my_netmask),  255,255,255,  0);
   IP4_ADDR((&my_gw),       192,168,  1,  1);
   netif_add(&my_netif, &my_ipaddr, &my_netmask, &my_gw, NULL, tapif_init, netif_input);
   netif_set_default(&my_netif);

On vient également configurer les callbacks qui permettent d’être informé d’un changement d’état de l’interface réseau :

  netif_set_status_callback(netif_default, my_status_callback);
  netif_set_link_callback(netif_default, my_link_callback);

Voici un exemple d’implémentation de ces callbacks :

static void my_status_callback(struct netif *state_netif)
{
   if (netif_is_up(state_netif)) {
      printf("status_callback==UP, local interface IP is %s\n", ip4addr_ntoa(netif_ip4_addr(state_netif)));
   } else {
      printf("status_callback==DOWN\n");
   }
}

static void my_link_callback(struct netif *state_netif)
{
   if (netif_is_link_up(state_netif)) {
      printf("link_callback==UP\n");
   } else {
      printf("link_callback==DOWN\n");
   }
}

Enfin on démarre l’interface ce qui aura pour effet d’exécuter tapif_init() qui avait été préalablement passée en paramètre de la fonction netif_add().

   netif_set_up(netif_default);

Ajout d’une connexion UDP

Maintenant que notre stack est initialisée, on peut lui ajouter une connexion de type UDP qui écoute sur le port 60001 quelque soit l’IP reçue.

On commence par allouer un nouveau descripteur de connexion UDP :

   my_udp_pcb = udp_new();
   if (NULL == my_udp_pcb) {
      printf("udp_new() has failed !\n");
      return -1;
   }

On se bind sur le bon couple IP/port:

   if (ERR_OK != udp_bind(my_udp_pcb, IP_ANY_TYPE, 60001)) {
      printf("udp_bind() has failed!\n");
      return -1;
   }

Puis on configure une callback de réception :

   udp_recv(my_udp_pcb, my_udp_recv_fn, NULL);

Polling de l’interface tapif

La stack est maintenant prête à recevoir les datagrammes UDP et à les “router” jusqu’à notre callback de réception. Il ne nous reste plus qu’à “exécuter” l’interface tapif, dans une boucle infinie, qui se chargera d’ ”injecter” dans la stack les trames réseaux reçues :

   while (1) {
      tapif_poll(&my_netif);
   }

Réception et émission UDP

Dans notre callback de réception, qui sera appelée par la stack à chaque fois qu’un datagramme UDP est reçu sur le port 60001, on peut commencer par imprimer quelques informations sur le buffer reçu :

static void my_udp_recv_fn (void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, u16_t port)
{
   printf("UDP packet received: \n");
   printf("from remote ip = %s\n", ip4addr_ntoa(addr));
   printf("from remote port = %d\n", port);
   printf("with data = %s", (char *)(p->payload));

Puis on alloue un buffer qui permettra de forger notre réponse :

   /* Alloc a new buffer to handle the response */
   struct pbuf * my_pbuf = pbuf_alloc(PBUF_TRANSPORT , 100, PBUF_RAM);
   if (NULL == my_pbuf) {
      printf("pbuf_alloc has failed!\n");
      return;
   }

On remplit ce buffer avec notre réponse :

   /* Fill the buffer with response */
   memcpy(my_pbuf->payload, "Ok, received: ", strlen("Ok, received: "));
   my_pbuf->len = strlen("Ok, received: ");
   memcpy(my_pbuf->payload + my_pbuf->len, p->payload, p->len);
   my_pbuf->len += p->len;
   my_pbuf->tot_len = my_pbuf->len;

On envoie notre réponse :

   /* Send the response to the sender */
   udp_sendto(pcb, my_pbuf, addr, port);

Et on termine en n’oubliant surtout pas de libérer les buffers pour éviter toute fuite mémoire :

   /* Free the buffers */
   pbuf_free(p);
   pbuf_free(my_pbuf);
}

Attention: même si le buffer p a été alloué par la stack et nous a été passé en paramètre, il est bien de la responsabilité de notre application de le libérer une fois que nous l’avons traité. La stack ne le libère pas automatiquement au retour d’appel de la callback de réception. Ce choix d’implémentation de lwIP permet à notre application, si elle le souhaite, de ne traiter le buffer que plus tard, dans un contexte différent de celui de la callback, sans avoir besoin de le recopier dans un autre buffer.

La compilation de notre projet avec cmake

Maintenant que nous avons le code source de notre application nous allons voir comment le compiler avec le support de cmake.

Dans notre fichier CMakeLists.txt on commence par définir la version minimale de cmake ainsi que le nom de notre projet :

cmake_minimum_required(VERSION 3.10)
project(my-lwip-app)

Ensuite il faut définir deux variables :

  • LWIP_DIR qui définit l’emplacement des sources de la stack lwIP
set (LWIP_DIR ${CMAKE_CURRENT_SOURCE_DIR}/lwip)
  • LWIP_INCLUDE_DIRS qui définit l’emplacement des headers:
    • de lwIP elle même (${LWIP_DIR}/src/include)
    • des contribs lwIP si on en a besoin (${LWIP_DIR}/contrib)
    • du portage utilisé (${LWIP_DIR}/contrib/ports/unix/port/include)
    • de notre fichier de configurtation "${CMAKE_CURRENT_SOURCE_DIR}"
set (LWIP_INCLUDE_DIRS
    "${LWIP_DIR}/src/include"
    "${LWIP_DIR}/contrib"
    "${LWIP_DIR}/contrib/ports/unix/port/include"
    "${CMAKE_CURRENT_SOURCE_DIR}"
)

On inclut les fichiers de la stack qui nous intéressent :

include(${LWIP_DIR}/contrib/ports/CMakeCommon.cmake)
include(${LWIP_DIR}/src/Filelists.cmake)
include(${LWIP_DIR}/contrib/Filelists.cmake)
include(${LWIP_DIR}/contrib/ports/unix/Filelists.cmake)

On déclare notre exécutable :

add_executable(my-lwip-app my-lwip-app.c)

On ajoute les includes de la stack à notre application :

target_include_directories(my-lwip-app PRIVATE ${LWIP_INCLUDE_DIRS})

Enfin on link notre application avec la stack et son portage sur unix :

target_link_libraries(my-lwip-app lwipcore lwipcontribportunix)

On peut maintenant compiler notre application avec les commandes cmake habituelles :

remy@remy-VirtualBox:~/test-lwip$ mkdir build
remy@remy-VirtualBox:~/test-lwip$ cd build/
remy@remy-VirtualBox:~/test-lwip/build$ cmake ..
-- The C compiler identification is GNU 9.3.0
-- The CXX compiler identification is GNU 9.3.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- CMAKE_BUILD_TYPE not set - defaulting to Debug build.
-- Build type: Debug
-- LWIP_MBEDTLSDIR not set - using default location /home/remy/test-lwip/lwip/../mbedtls
-- Could NOT find Doxygen (missing: DOXYGEN_EXECUTABLE) 
-- Doxygen needs to be installed to generate the doxygen documentation
-- Configuring done
-- Generating done
-- Build files have been written to: /home/remy/test-lwip/build
remy@remy-VirtualBox:~/test-lwip/build$ make
Scanning dependencies of target lwipcore
[  1%] Building C object CMakeFiles/lwipcore.dir/lwip/src/core/init.c.o
[  3%] Building C object CMakeFiles/lwipcore.dir/lwip/src/core/def.c.o
[  3%] Building C object CMakeFiles/lwipcore.dir/lwip/src/core/dns.c.o
[  5%] Building C object CMakeFiles/lwipcore.dir/lwip/src/core/inet_chksum.c.o
[  5%] Building C object CMakeFiles/lwipcore.dir/lwip/src/core/ip.c.o

...

[ 95%] Building C object CMakeFiles/lwipcontribportunix.dir/lwip/contrib/ports/unix/port/netif/sio.c.o
[ 96%] Building C object CMakeFiles/lwipcontribportunix.dir/lwip/contrib/ports/unix/port/netif/fifo.c.o
[ 96%] Linking C static library liblwipcontribportunix.a
[ 96%] Built target lwipcontribportunix
Scanning dependencies of target my-lwip-app
[ 98%] Building C object CMakeFiles/my-lwip-app.dir/my-lwip-app.c.o
[100%] Linking C executable my-lwip-app
[100%] Built target my-lwip-app

Test de notre application

On monte l’interface tapif et on démarre notre exécutable :

remy@remy-VirtualBox:~/test-lwip/build$ source ../lwip/contrib/ports/unix/setup-tapif 
[sudo] password for remy: 
remy@remy-VirtualBox:~/test-lwip/build$ ./my-lwip-app 
Hello World!
status_callback==UP, local interface IP is 192.168.1.200

Dans un second terminal on utilise la commande netcat pour envoyer un datagramme UDP vers notre application et recevoir sa réponse :

remy@remy-VirtualBox:~$ nc -u 192.168.1.200 60001
salut
Ok, received: salut

Conclusion

Dans ce premier article nous avons vu les bases qui permettent la manipulation de la stack lwIP et de son portage sur un environnement Linux.

Pour aller plus loin je vous invite à explorer et à exécuter, toujours sur votre distribution Linux préférée, les différents exemples contenus dans lwip/contrib/examples afin de découvrir l’utilisation de la couche TCP, du DNS ou des api “netconn” ou “socket” par exemple.

Dans un prochain article nous verrons comment réaliser le portage de la stack sur un microcontrôleur (STM32) avec le support d’un RTOS (FreeRTOS).

A bientôt !

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée.