Logs
Consultez les logs.
OK
Liste des données
Consultez la liste des données.
OK
Loading...
Formulaire
Saisissez vos données.
Enregistrer
Annuler

Programmation réseau pratique avec C

Vues
148

La programmation réseau est un sujet passionnant, mais aussi très profond ; il se passe beaucoup de choses à de nombreux niveaux. Certains langages de programmation masquent ces abstractions. Avec le langage Python, par exemple, vous pouvez télécharger une page web entière en une seule ligne de code. Ce n'est pas le cas en C ! En C, pour télécharger une page web, il est essentiel de tout connaître. Il faut connaître les sockets, le protocole TCP (Transfer Control Protocol) et HTTP. En programmation réseau en C, rien n'est caché.

Le C est un excellent langage pour apprendre la programmation réseau. Non seulement parce qu'il permet de voir tous les détails, mais aussi parce que les systèmes d'exploitation les plus répandus utilisent tous des noyaux écrits en C. Aucun autre langage ne vous offre un accès aussi performant que le C. En C, tout est sous votre contrôle : vous pouvez organiser vos structures de données exactement comme vous le souhaitez, gérer la mémoire avec précision et même vous tirer une balle dans le pied comme bon vous semble.

En matière de programmation réseau, on ne peut pas se contenter de l'approche « ça marche, c'est suffisant » de la programmation par coïncidence. Il faut utiliser le raisonnement.

Dans ce tutoriel, je m'attache à aborder la programmation réseau de manière moderne et sécurisée. Les programmes d'exemple sont soigneusement conçus pour fonctionner avec IPv4 et IPv6, et sont tous écrits de manière portable et indépendante du système d'exploitation, dans la mesure du possible.

Dès qu'il existe un risque d'erreur de mémoire, je m'efforce d'en tenir compte et de le signaler. La sécurité est trop souvent négligée. Je suis convaincu que la sécurité est importante et qu'elle doit être intégrée au système dès le départ. C'est pourquoi, en plus d'enseigner les bases du réseau, ce tutoriel aborde en détail les protocoles sécurisés, tels que TLS. J'espère que vous prendrez autant de plaisir à lire ce tutoriel.  



Contenu de ce tutoriel


La section, « Introduction aux réseaux et aux protocoles », présente les concepts importants liés aux réseaux. Cette section inclut des exemples de programmes pour déterminer votre adresse IP de manière pragmatique.

La section, « Se familiariser avec les API Socket », présente les API de programmation de sockets et vous permet de créer votre premier programme réseau : un petit serveur web. La section, « Présentation approfondie des connexions TCP », se concentre sur la programmation des sockets TCP. Cette section contient des exemples de programmes développés pour les côtés client et serveur.

La section, « Établissement de connexions UDP », aborde la programmation avec les sockets UDP (User Datagram Protocol). La section, « Résolution de noms d'hôtes et DNS », explique comment les noms d'hôtes sont traduits en adresses IP. Cette section présente un exemple de programme permettant d'effectuer des recherches DNS manuelles à l'aide d'UDP.

La section, « Création d'un client web simple », présente HTTP, le protocole qui alimente les sites web. Nous nous lançons directement dans la création d'un client HTTP en C.

La section, « Construire un serveur web simple », décrit comment construire un serveur web entièrement fonctionnel en C. Ce programme est capable de servir un site web statique à n'importe quel navigateur web moderne.

La section, « Mettre votre programme en route pour l'envoi d'e-mails », décrit le protocole SMTP (Simple Mail Transfer Protocol), qui gère les e-mails. Dans cette section, nous développons un programme permettant d'envoyer des e-mails sur Internet.

La section, « Charger des pages web sécurisées avec HTTPS et OpenSSL », explore TLS, le protocole qui sécurise les pages web. Dans cette section, nous développons un client HTTPS capable de télécharger des pages web en toute sécurité.

La section, « Implémentation d'un serveur web sécurisé », poursuit le thème de la sécurité et explore la construction d'un serveur web HTTPS sécurisé.

La section, « Établissement de connexions SSH avec libssh », poursuit le thème du protocole sécurisé. L'utilisation de Secure Shell (SSH) est abordée pour se connecter à un serveur distant, exécuter des commandes et télécharger des fichiers en toute sécurité.

La section, « Surveillance et sécurité du réseau », présente les outils et techniques utilisés pour tester les fonctionnalités du réseau, résoudre les problèmes et intercepter les protocoles de communication non sécurisés.

La section, « Conseils et pièges de la programmation par socket », détaille TCP et aborde de nombreux cas limites importants liés à la programmation par socket. Les techniques abordées sont précieuses pour créer des programmes réseau robustes.

La section, « Programmation Web pour l'Internet des Objets », offre un aperçu de la conception et de la programmation des applications de l'Internet des Objets (IoT). L'annexe, « Réponses aux questions », répond aux questions de compréhension posées à la fin de chaque section.

L'annexe, « Configuration de votre compilateur C sous Windows », explique comment configurer l'environnement de développement Windows nécessaire à la compilation de tous les programmes d'exemple de ce tutoriel.

L'annexe, « Configuration de votre compilateur C sous Linux », fournit les instructions de configuration pour préparer votre ordinateur Linux à compiler tous les programmes d'exemple de ce tutoriel.

L'annexe, « Configuration de votre compilateur C sous macOS », explique étape par étape comment configurer votre système macOS afin qu'il puisse compiler tous les programmes d'exemple de ce tutoriel.


Introduction aux réseaux et aux protocoles



Détermination des routeurs de transit


Nous souhaitons déterminer les routeurs par lesquels transitent les messages sortant de notre machine vers une machine distante (readydev.ovh).

Nous lançons la commande de traçage (tracert) en indiquant la machine de destination (readydev.ovh) sous Windows (Voir Extrait 1 : Ligne 4).

Nous lançons la commande de traçage (traceroute) en indiquant la machine de destination (readydev.ovh) sous Linux (Voir Extrait 1 : Ligne 8).

Extrait 1 : Affichage des routeurs de transit vers une machine distante
...
Sous WIndows
...
> tracert readydev.ovh
...
Sous Linux
...
$ traceroute readydev.ovh
...

Détermination de l'adresse IP


Nous souhaitons déterminer l'adresse IP de notre système.

Nous lançons la commande (ipconfig) pour afficher les adresses IP de nos cartes réseau sous Windows (Voir Extrait 1 : Ligne 4).

Nous lançons la commande (ifconfig) pour afficher les adresses IP de nos cartes réseau sous Linux (Voir Extrait 1 : Ligne 8). C'est l'ancienne procédure, obsolète aujourd'hui.

Nous pouvons aussi lancer la commande (ip addr) pour afficher les adresses IP de nos cartes réseau sous Linux (Voir Extrait 1 : Ligne 9). C'est la nouvelle procédure, recommandée.

Nous lançons la commande (ifconfig) pour afficher les adresses IP de nos cartes réseau sous MacOS (Voir Extrait 1 : Ligne 13).

Extrait 1 : Détermination de l'adresse IP
...
> ipconfig
...
Sous Linux
...
$ ifconfig
$ ip addr
...
Sous MacOS
...
$ ifconfig
...

Détermination de l'adresse IP publique


Nous souhaitons déterminer l'adresse IP publique de notre système.

Nous cliquons sur l'un des liens ci-dessous pour afficher l'adresse IP publique de notre machine (Voir Extrait 1 : Ligne 2,3,4).  

Extrait 1 : Détermination de l'adresse IP
...
- http://api.ipify.org/
- http://icanhazip.com/
- http://ifconfig.me/ip
...

Initialisation et libération de la librairie réseau sous Windows


Nous souhaitons initialiser et libérer la librairie (Winsock2) chargée de la communication réseau sous notre système Windows à partir d'un programme C.

Nous incluons le fichier d'entête C de la librairie (Winsock2) (Voir Extrait 1 : Ligne 3).

Nous éditons les liens avec la libraire (Winsock2) à partir de la directive (#pragma comment) en indiquant la librairie (ws2_32.lib) (Voir Extrait 1 : Ligne 5).

Nous notons que, la directive (#pragma comment) est prise en compte par le compilateur MSVC, mais ignorée par le compilateur MinGW.

Nous construisons la version de la librairie (Winsock2) à utiliser grâce à la macro (MAKEWORD) en indiquant la version majeur (2) dans le premier argument et la version mineur (2) dans le deuxième argument pour correspondre à la version (2.2) et nous initialisons la libraire (Winsock2) grâce à la fonction (WSAStartup) (Voir Extrait 1 : Ligne 10).

Nous libérons la mémoire interne allouée par la librairie (Winsock2) grâce à la fonction (WSACleanup) à la fin de l'opération (Voir Extrait 1 : Ligne 16).

Extrait 1 : Initialisation et libération de la librairie (Winsock2)
...
#include <stdio.h>
#include <winsock2.h>

#pragma comment(lib, "ws2_32.lib")

int main()
{
    WSADATA d;
    if (WSAStartup(MAKEWORD(2, 2), &d))
    {
        printf("Failed to initialize.\n");
        return -1;
    }

    WSACleanup();
    printf("Ok.\n");
    return 0;
}
...

Nous démarrons la compilation grâce à la commande (gcc) en indiquant le fichier source (main.c) et le nom du fichier exécutable (setup.exe) et nous éditions les liens avec la librairie (Winsock2) pour le compilateur MinGW sous Windows (Voir Extrait 2 : Ligne 2).

Extrait 2 : Compilation et édition des liens avec la librairie (Winsock2)
...
> gcc main.c -o setup.exe -lws2_32
...

Détermination de la liste des cartes réseau sous Windows


Nous souhaitons lister les cartes réseau sous notre système Windows à partir d'un programme C basé sur la librairie (Winsock2).

Nous définissons la macro (WIN32_WINNT) afin de pouvoir inclure les versions appropriées des fichiers d'entête C sous Windows (Voir Extrait 1 : Ligne 3).

Nous incluons les fichiers d'entête C de la librairie (Winsock2) (Voir Extrait 1 : Ligne 6,8).

Nous incluons les fichiers d'entête C de la librairie (IP Helper) (Voir Extrait 1 : Ligne 7).

Nous éditons par programmation les liens avec la librairie (Winsock2) grâce à la directive (#pragma comment) en indiquant la librairie (ws2_32.lib) (Voir Extrait 1 : Ligne 12).

Nous éditons par programmation les liens avec la librairie (IP Helper) grâce à la directive (#pragma comment) en indiquant la librairie (iphlpapi.lib) (Voir Extrait 1 : Ligne 13).

Nous initialisons la librairie (Winsock2) (Voir Extrait 1 : Ligne 18).

Nous créons dynamiquement de la mémoire pour les adaptateurs réseau (adapters) de type (PIP_ADAPTER_ADDRESSES) grâce à la fonction (malloc) en indiquant la taille de l'espace mémoire (asize) (Voir Extrait 1 : Ligne 28).

Nous réservons une mémoire de 20.000 octets (Voir Extrait 1 : Ligne 24).

Nous récupérons la liste chaînée de tous les adaptateurs réseau installés sur notre système Windows grâce à la méthode (GetAdaptersAddresses) en indiquant que nous souhaitons des adresses réseau IPv4 et IPv6 (AF_UNSPEC) (Voir Extrait 1 : Ligne 37).

Nous pouvons aussi spécifier (AF_INET) pour les adresses réseau IPv4 uniquement ou (AF_INET6) pour les adresses réseau IPv6 uniquement. La fonction (GetAdaptersAddresses) nous renvoie le code d'erreur (ERROR_BUFFER_OVERFLOW) si taille de la mémoire allouée (asize) est insuffisante pour stocker toutes les adresses des adaptateurs réseau disponibles (Voir Extrait 1 : Ligne 39). Dans ce cas la variable (asize) est modifiée pour stocker la taille de la mémoire requise.

Nous libérons la mémoire allouée par les adaptateurs réseau (adapters) pour reprendre l'opérations (Voir Extrait 1 : Ligne 42). La fonction (GetAdaptersAddresses) nous renvoie le code de succès (ERROR_SUCCESS) si l'opération s'est bien déroulée (Voir Extrait 1 : Ligne 44). Dans ce cas, nous cassons la boucle avec l'instruction (break) pour continuer l'opération (Voir Extrait 1 : Ligne 46).

Nous récupérons le pointeur direct vers le premier adaptateur réseau (adapter) (Voir Extrait 1 : Ligne 57).

Nous entrons dans une boucle pour parcourir la liste des adaptateurs réseau (adapter) (Voir Extrait 1 : Ligne 58).

Nous affichons le nom de l'adaptateur réseau (FriendlyName) à partir du gestionnaire (adapter) (Voir Extrait 1 : Ligne 60).

Nous récupérons le pointeur direct vers l'adresse du premier adaptateur réseau (address) de type (PIP_ADAPTER_UNICAST_ADDRESS) à partir du gestionnaire (adapter) en indiquant le pointeur (FirstUnicastAddress) (Voir Extrait 1 : Ligne 62).

Nous entrons dans une boucle pour parcourir la liste des adresses des adaptateurs réseau (address) (Voir Extrait 1 : Ligne 63).

Nous récupérons le type de l'adresse IP (sa_family) à partir du gestionnaire (address) et nous affichons s'il s'agit d'une adresse IPv4 ou IPv6 (Voir Extrait 1 : Ligne 66).

Nous récupérons la chaîne de l'adresse IP (ap) grâce à la fonction (getnameinfo) en indiquant l'adresse du socket réseau (lpSockaddr) à partir du gestionnaire (address), la taille en octet de l'adresse du socket réseau (iSockaddrLength) à partir du gestionnaire (address) (Voir Extrait 1 : Ligne 71).

Nous affichons la chaîne de l'adresse IP de l'adaptateur réseau (Voir Extrait 1 : Ligne 74).

Nous récupérons l'adresse de l'adaptateur réseau suivante (address) à partir du gestionnaire (address) en indiquant le pointeur suivant (Next) (Voir Extrait 1 : Ligne 71).

Nous récupérons l'adaptateur réseau suivant (adapter) à partir du gestionnaire (adapter) en indiquant le pointeur (Next) (Voir Extrait 1 : Ligne 79).

Nous libérons la mémoire allouée par les adaptateurs réseau (adapters) (Voir Extrait 1 : Ligne 82).

Nous libérons la mémoire allouée par la librairie (Winsock2) (Voir Extrait 1 : Ligne 83).

Extrait 1 : Détermination de la liste des cartes réseau sous Windows
...
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0600
#endif

#include <winsock2.h>
#include <iphlpapi.h>
#include <ws2tcpip.h>
#include <stdio.h>
#include <stdlib.h>

#pragma comment(lib, "ws2_32.lib")
#pragma comment(lib, "iphlpapi.lib")

int main()
{
    WSADATA d;
    if (WSAStartup(MAKEWORD(2, 2), &d))
    {
        printf("Failed to initialize.\n");
        return -1;
    }

    DWORD asize = 20000;
    PIP_ADAPTER_ADDRESSES adapters;
    do
    {
        adapters = (PIP_ADAPTER_ADDRESSES)malloc(asize);

        if (!adapters)
        {
            printf("Couldn't allocate %ld bytes for adapters.\n", asize);
            WSACleanup();
            return -1;
        }

        int r = GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_INCLUDE_PREFIX, 0,
                                     adapters, &asize);
        if (r == ERROR_BUFFER_OVERFLOW)
        {
            printf("GetAdaptersAddresses wants %ld bytes.\n", asize);
            free(adapters);
        }
        else if (r == ERROR_SUCCESS)
        {
            break;
        }
        else
        {
            printf("Error from GetAdaptersAddresses: %d\n", r);
            free(adapters);
            WSACleanup();
            return -1;
        }
    } while (!adapters);

    PIP_ADAPTER_ADDRESSES adapter = adapters;
    while (adapter)
    {
        printf("\nAdapter name: %S\n", adapter->FriendlyName);

        PIP_ADAPTER_UNICAST_ADDRESS address = adapter->FirstUnicastAddress;
        while (address)
        {
            printf("\t%s",
                   address->Address.lpSockaddr->sa_family == AF_INET ? 
                   "IPv4" : "IPv6");

            char ap[100];

            getnameinfo(address->Address.lpSockaddr,
                        address->Address.iSockaddrLength,
                        ap, sizeof(ap), 0, 0, NI_NUMERICHOST);
            printf("\t%s\n", ap);

            address = address->Next;
        }

        adapter = adapter->Next;
    }

    free(adapters);
    WSACleanup();
    return 0;
}
...

Nous compilons le projet pour le compilateur MinGW (Voir Extrait 2 : Ligne 2).

Extrait 2 : Compilation et édition des liens du projet sous MinGW   
...
> gcc main.c -o setup.exe -liphlpapi -lws2_32
...

Détermination de la liste des cartes réseau sous Linux


Nous souhaitons lister les cartes réseau sous notre système Linux ou MacOS à partir d'un programme C basé sur la librairie (socket).

Nous incluons les fichiers d'entête C de la librairie (socket) (Voir Extrait 1 : Ligne 2).

Nous récupérons la liste chainée des interfaces réseau (addresses) de type (ifaddrs) grâce à la fonction (getifaddrs) (Voir Extrait 1 : Ligne 12).

Nous récupérons le pointeur direct vers la première interface réseau (address) à partir du gestionnaire (Voir Extrait 1 : Ligne 18).

Nous entrons dans une boucle pour parcourir la liste des interfaces réseau (address) (Voir Extrait 1 : Ligne 19).

Nous récupérons le type de l'interface réseau (family) à partir du gestionnaire (address) en indiquant le type (sa_family) (Voir Extrait 1 : Ligne 26).

Nous affichons le nom de l'interface réseau (ifa_name) à partir du gestionnaire (address) (Voir Extrait 1 : Ligne 26).

Nous vérifions le type de l'interface réseau (family) pour afficher s'il s'agit d'une adresse IPv4 ou IPv6 (Voir Extrait 1 : Ligne 33).

Nous récupérons la taille en octets de l'adresse du socket réseau IPv4 ou IPv6 (family_size) en fonction du type (family) (Voir Extrait 1 : Ligne 30).

Nous récupérons la chaîne de l'adresse IP de l'interface réseau (ap) grâce à la fonction (getnameinfo) en indiquant l'adresse du socket réseau (ifa_addr) à partir du gestionnaire (address) et la taille en octet de l'adresse du socket réseau (family_size) (Voir Extrait 1 : Ligne 35).

Nous affichons la chaîne de l'adresse IP de l'interface réseau (ap) (Voir Extrait 1 : Ligne 37).

Nous récupérons l'adresse de l'interface réseau suivante (address) à partir du gestionnaire (address) en indiquant le pointeur suivant (ifa_next) (Voir Extrait 1 : Ligne 39).

Nous libérons la mémoire allouée par le gestionnaire d'interfaces réseau (addresses) (Voir Extrait 1 : Ligne 42).

Extrait 1 : Détermination de la liste des cartes réseau sous Linux ou MacOS
...
#include <sys/socket.h>
#include <netdb.h>
#include <ifaddrs.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
    struct ifaddrs *addresses;

    if (getifaddrs(&addresses) == -1)
    {
        printf("getifaddrs call failed\n");
        return -1;
    }

    struct ifaddrs *address = addresses;
    while (address)
    {
        if (address->ifa_addr == NULL)
        {
            address = address->ifa_next;
            continue;
        }
        int family = address->ifa_addr->sa_family;
        if (family == AF_INET || family == AF_INET6)
        {
            printf("%s\t", address->ifa_name);
            printf("%s\t", family == AF_INET ? "IPv4" : "IPv6");

            char ap[100];
            const int family_size = family == AF_INET ? sizeof(struct 
            sockaddr_in) : sizeof(struct sockaddr_in6);
            getnameinfo(address->ifa_addr,
                        family_size, ap, sizeof(ap), 0, 0, NI_NUMERICHOST);
            printf("\t%s\n", ap);
        }
        address = address->ifa_next;
    }

    freeifaddrs(addresses);
    return 0;
}
...

Nous compilons le projet sous Linux ou MacOS (Voir Extrait 2 : Ligne 2).

Extrait 2 : Compilation du projet sous Linux ou MacOS
...
$ gcc main.c -o setup
...


À suivre


À suivre...