Étudiants

  • Youssef EL KADI
  • Mathéo GALUBA

TPs réalisés

TP2 - Utilisations de sockets de type RAW

1 - Préambule

Exercice 1

Le fichier /usr/include/netinet/ip.h contient une structure nommée struct iphdr. Étudiez cette structure et comparez-la au format de la trame IP que vous trouverez sur Internet.

struct iphdr
{
#if __BYTE_ORDER == __LITTLE_ENDIAN
  unsigned int ihl:4;
  unsigned int version:4;
#elif __BYTE_ORDER == __BIG_ENDIAN
  unsigned int version:4;
  unsigned int ihl:4;
#else
  # error "Please fix <bits/endian.h>"
#endif
  uint8_t tos;
  uint16_t tot_len;
  uint16_t id;
  uint16_t frag_off;
  uint8_t ttl;
  uint8_t protocol;
  uint16_t check;
  uint32_t saddr;
  uint32_t daddr;
  /*The options start here. */
};

Source: https://www.malekal.com/quelle-est-la-structure-paquet-tcp-ip/

ChampTaille (bits)Description
version4Numéro de version du protocole IP
ihl (Internet Header Length)4Longueur de l’en-tête en mots de 32 bits (1 mot = 4 octets)
tos (Type of Service)8Ancien champ de “Type de service”, remplacé par le champ DSCP (Differentiated Services Code Point)
tot_len (Total Length)16Taille totale du paquet IP (en-tête + données) en octets
id (Identification)16Identifiant du paquet
frag_off (Fragment Offset + Flags)16Contient à la fois les drapeaux de fragmentation et le décalage du fragment
ttl (Time To Live)8Durée de vie du paquet (en nombre de sauts)
protocol8Protocole de la couche supérieure (ICMP, TCP, UDP, etc.)
check (Header Checksum)16Somme de contrôle de l’en-tête IP
saddr (Source Address)32Adresse IP source (IPv4)
daddr (Destination Address)32Adresse IP de destination
options (non inclus ici)variableChamps optionnels (sécurité, routage, timestamp, etc.)

Exercice 2

Le fichier /usr/include/netinet/ip_icmp.h contient une structure nommée struct icmphdr. Étudiez cette structure et comparez-la au format de la trame ICMP que vous trouverez sur Internet.

struct icmphdr
{
  uint8_t type;     /* message type */
  uint8_t code;     /* type sub-code */
  uint16_t checksum;
  union
  {
    struct
    {
      uint16_t  id;
      uint16_t  sequence;
    } echo;         /* echo datagram */
    uint32_t    gateway;    /* gateway address */
    struct
    {
      uint16_t  __glibc_reserved;
      uint16_t  mtu;
    } frag;         /* path mtu discovery */
  } un;
};

Source: https://tecadmin.net/icmp-internet-control-message-protocol/

ChampTaille (bits)Description
type8Type du message ICMP
code8Sous-type (ou “code”)
checksum16Somme de contrôle ICMP
un (union)variableContient différents sous-champs selon le type ICMP

Exercice 3

Lisez la documentation des fonctions htons et ntohs.

The htons() function converts the unsigned short integer hostshort from host byte order to network byte order.

The ntohs() function converts the unsigned short integer netshort from network byte order to host byte order.

Ces deux fonctions servent à convertir l’ordre des octets entre “host byte order” (spécifique à chaque machine suivant les détails d’implémentation) et “network byte order” (indépendant de la machine).

Source: https://www.man7.org/linux/man-pages/man3/htons.3.html Source: https://www.ibm.com/docs/en/zvm/7.2.0?topic=domains-network-byte-order-host-byte-order

2 - Création des fonctions de manipulation

2.1 - Affichage d’un tableau d’octets

Exercice 4

Écrivez une fonction void display(char * buffer, int length) qui affiche les length octets du tableau buffer, en affichant les octets en hexadécimal, deux par deux, et seize par lignes.

// Exercice 4: Affichage d'un tableau d'octets
void display(char *buffer, int length) {
  printf("Contenu du buffer (%d octets):\n", length);
 
  for (int i = 0; i < length; i++) {
    printf("%02x", (unsigned char)buffer[i]);
 
    // Ajouter un espace tous les 2 octets (sauf à la fin)
    if (i % 2 == 1 && i < length - 1) {
      printf(" ");
    }
 
    // Retour à la ligne tous les 16 octets
    if ((i + 1) % 16 == 0) {
      printf("\n");
    }
  }
 
  if (length % 16 != 0) {
    printf("\n");
  }
  printf("\n");
}

2.2 - Entête IP

Exercice 5

Écrivez une fonction qui remplit une structure struct iphdr.

// Exercice 5: Remplir une structure iphdr
void fill_iphdr(struct iphdr *ip, uint32_t saddr, uint32_t daddr,
                uint16_t tot_len, uint8_t protocol) {
  ip->version = 4;              // IPv4
  ip->ihl = 5;                  // Header length = 5 * 4 = 20 octets
  ip->tos = 0;                  // Type of Service
  ip->tot_len = htons(tot_len); // Longueur totale (en network byte order)
  ip->id = htons(0x1234);       // Identifiant du paquet
  ip->frag_off = 0;             // Pas de fragmentation
  ip->ttl = 100;                // Time To Live
  ip->protocol = protocol;      // Protocole (ICMP = 1)
  ip->check = 0;                // Sera calculé plus tard
  ip->saddr = saddr;            // Adresse source
  ip->daddr = daddr;            // Adresse destination
}

2.3 - Entête ICMP

Exercice 6

Écrivez une fonction qui remplit une structure struct icmphdr.

// Exercice 6: Remplir une structure icmphdr
void fill_icmphdr(struct icmphdr *icmp, uint8_t type, uint8_t code, uint16_t id,
                  uint16_t sequence) {
  icmp->type = type;                        // Type du message ICMP
  icmp->code = code;                        // Code (sous-type)
  icmp->checksum = 0;                       // Sera calculé plus tard
  icmp->un.echo.id = htons(id);             // Identifiant echo
  icmp->un.echo.sequence = htons(sequence); // Numéro de séquence
}

2.4 - Sommes de contrôle

Exercice 7

Implémentez la fonction short checksum(char * addr, int count) qui calcule la somme de contrôle selon l’algorithme.

// Exercice 7: Calcul de la somme de contrôle
// Source: https://www.rfc-editor.org/rfc/rfc1071
short checksum(char *addr, int count) {
  /* Compute Internet Checksum for "count" bytes
   *         beginning at location "addr".
   */
  register long sum = 0;
 
  while (count > 1) {
    /*  This is the inner loop */
    sum += *(unsigned short *)addr;
    addr += 2;
    count -= 2;
  }
 
  /*  Add left-over byte, if any */
  if (count > 0) {
    sum += *(unsigned char *)addr * 256;
  }
 
  /*  Fold 32-bit sum to 16 bits */
  while (sum >> 16) {
    sum = (sum & 0xffff) + (sum >> 16);
  }
 
  short checksum = ~sum;
 
  return checksum;
}

Exercice 8

Après avoir rempli les structures struct iphdr et struct icmphdr dans un tableau d’octets, mettez à jour la somme de contrôle d’ICMP. Cette somme de contrôle porte sur l’ensemble du paquet ICMP (incluant ses éventuelles données).

// Exercice 8: Mettre à jour la somme de contrôle ICMP
void update_icmp_checksum(char *icmp, int length) {
  struct icmphdr *icmp_hdr = (struct icmphdr *)icmp;
  icmp_hdr->checksum = 0;
  icmp_hdr->checksum = checksum(icmp, length);
}

Exercice 9

Après avoir mis à jour la somme de contrôle d’ICMP, mettez a jour la somme de contrôle IP. Cette somme de contrôle porte sur l’ensemble du paquet IP (incluant le paquet ICMP et ses éventuelles données).

// Exercice 9: Mettre à jour la somme de contrôle IP
void update_ip_checksum(char *ip, int length) {
  struct iphdr *ip_hdr = (struct iphdr *)ip;
  ip_hdr->check = 0;
  ip_hdr->check = checksum(ip, length);
}

2.5 - Vérification

Exercice 10

En choisissant les mêmes paramètres que ceux indiqués au-dessus, vérifiez que votre programme affiche bien les octets suivants, avant le calcul des sommes de contrôle :

4500 001c 1234 0000 6401 0000 c0a8 0001
c0a8 0002 0800 0000 5678 0001 

Sortie de notre programme :

=== Exercice 10: Avant calcul des sommes de contrôle ===
Contenu du buffer (28 octets):
4500 001c 1234 0000 6401 0000 c0a8 0001 
c0a8 0002 0800 0000 5678 0001

Exercice 11

En choisissant les mêmes paramètres que ceux indiqués au-dessus, vérifiez que votre programme affiche bien les octets suivants, après le calcul des sommes de contrôle :

4500 001c 1234 0000 6401 c65c c0a8 0001
c0a8 0002 0800 a186 5678 0001

Sortie de notre programme :

=== Exercice 11: Après calcul des sommes de contrôle ===
Contenu du buffer (28 octets):
4500 001c 1234 0000 6401 c359 c0a8 0001 
c0a8 0002 0800 a186 5678 0001

On notera toutefois que les sommes de contrôle diffèrent légèrement, mais nous avons bien validé la nôtre et l’erreur se trouve dans la consigne (aide utilisé : https://en.wikipedia.org/wiki/Internet_checksum).

3 - Envoi du paquet ECHO_REQUEST

Exercice 12

Créez une socket pour la famille PF_INET, de type SOCK_RAW et concernant le protocole IPPROTO_RAW. Vous penserez à tester le retour de la fonction et à utiliser perror en cas d’erreur.

int sock_send = socket(PF_INET, SOCK_RAW, IPPROTO_RAW);
if (sock_send < 0) {
  perror("socket");
  exit(EXIT_FAILURE);
}
printf("Socket RAW créée avec succès (fd: %d)\n", sock_send);

Exercice 13

Envoyez le paquet sur la socket créée. Vous penserez à tester le retour de la fonction et à utiliser perror en cas d’erreur.

struct sockaddr_in dest_addr;
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = 0;
dest_addr.sin_addr.s_addr = daddr;
// dest_addr.sin_zero est déjà initialisé à 0 par memset
if (sendto(sock_send, buffer, tot_len, 0, (struct sockaddr *)&dest_addr,
           sizeof(struct sockaddr_in)) < 0) {
  perror("sendto");
  exit(EXIT_FAILURE);
}
printf("Paquet envoyé avec succès à %s\n", inet_ntoa(dest_addr.sin_addr));

4 - Réception du paquet ECHO_REPLY

Pour cette partie, nous avons décidé de créer un deuxième exécutable. Nous avons donc sender pour envoyer le paquet et receiver qui écoute une réponse de manière bloquante.

Exercice 14

Créez une autre socket de type SOCK_RAW mais de protocole IPPROTO_ICMP à présent.

int sock_rec = socket(PF_INET, SOCK_RAW, IPPROTO_ICMP);
if (sock_rec < 0) {
  perror("socket");
  exit(EXIT_FAILURE);
}

Exercice 15

Utilisez la fonction recvfrom pour recevoir la réponse éventuelle de votre ping.

struct sockaddr_in from;
socklen_t fromlen = sizeof(from);
ssize_t rec_len = recvfrom(sock_rec, buffer, sizeof(buffer), 0,
                           (struct sockaddr *)&from, &fromlen);
if (rec_len < 0) {
  perror("recvfrom");
  exit(EXIT_FAILURE);
}

Améliorations

Récupération automatique de l’adresse IP source (machine locale)

Nous avons implémenté une fonction get_local_ip() qui retourne l’IPv4 locale afin d’éviter son entrée manuelle par l’utilisateur ou le code en dure de cette IP dans le code.

uint32_t get_local_ip() {
  int sock = socket(AF_INET, SOCK_DGRAM, 0);
  if (sock < 0) {
    perror("socket (local IP)");
    exit(EXIT_FAILURE);
  }
 
  struct sockaddr_in temp_addr;
  memset(&temp_addr, 0, sizeof(temp_addr));
  temp_addr.sin_family = AF_INET;
  temp_addr.sin_port = htons(80);
  inet_pton(AF_INET, "8.8.8.8", &temp_addr.sin_addr);
 
  if (connect(sock, (struct sockaddr *)&temp_addr, sizeof(temp_addr)) < 0) {
    perror("connect (local IP)");
    close(sock);
    exit(EXIT_FAILURE);
  }
 
  struct sockaddr_in local_addr;
  socklen_t addr_len = sizeof(local_addr);
  if (getsockname(sock, (struct sockaddr *)&local_addr, &addr_len) < 0) {
    perror("getsockname");
    close(sock);
    exit(EXIT_FAILURE);
  }
 
  close(sock);
  return local_addr.sin_addr.s_addr;
}

Passage d’un paramètre ‘destination’

Notre exécutable prend en paramètre l’IPv4 de destination à la manière de la commande ping sous GNU+Linux.

Calcul du temps de réponse

Nous avons ajouté un calcul du temps entre l’envoi et la réception et nous affichons le résultat à l’utilisateur en milliseconde.

struct timeval start, end;
gettimeofday(&start, NULL);
 
// [...] Envoi
 
printf("PING à %s\n", inet_ntoa(dest_addr.sin_addr));
 
// [...] Reception
 
gettimeofday(&end, NULL);
 
long sec = end.tv_sec - start.tv_sec;
long usec = end.tv_usec - start.tv_usec;
double elapsed_ms = (sec * 1000.0) + (usec / 1000.0);
 
printf("PONG de %s en %.3f ms (%zd octets)\n", inet_ntoa(from.sin_addr),
       elapsed_ms, rec_len);

Fermeture des sockets

Bien que ça ne soit pas préciser dans le sujet, nous appelons la fonction close() sur toutes les sockets une fois leur utilisation terminées.

 
uint32_t get_local_ip() {
  // [...]
 
  close(sock);
 
  return local_addr.sin_addr.s_addr;
}
 
int main(int argc, char *argv[]) {
 
  // [...]
 
  close(sock_send);
  close(sock_recv);
 
  return EXIT_SUCCESS;
}

TP5 - Implémentation d’un sniffer réseau

Exercice 1

Installez tcpcdump sur votre machine.

Exercice 2

Exécutez tcpdump -i eth0 ip et chargez la page web de Google. Que se passe-t’il ? Pensez à remplacer, si besoin, eth0 par l’interface amenant à votre connexion Internet.

sudo tcpdump -i wlan0 ip

Nous utilisons wlan0 car notre connexion Internet passe par le WiFi.

Lorsqu’on charge la page Google (https://www.google.com), tcpdump capture et affiche tous les paquets IP échangés. On observe :

Requêtes DNS

Résolution du nom de domaine www.google.com

13:08:53.458229 IP archlinux-matheo.59416 > _gateway.domain: 32098+ A? www.google.com. (32)
13:08:53.458242 IP archlinux-matheo.59416 > _gateway.domain: 59232+ AAAA? www.google.com. (32)
13:08:53.492594 IP _gateway.domain > archlinux-matheo.59416: 32098 1/0/0 A 216.58.205.196 (48)
13:08:53.493683 IP _gateway.domain > archlinux-matheo.59416: 59232 1/0/0 AAAA 2a00:1450:4006:807::2004 (60)
  • Double requête DNS : Type A (IPv4) et AAAA (IPv6)
  • Réponses obtenues :
    • IPv4 : 216.58.205.196
    • IPv6 : 2a00:1450:4006:807::2004

Résolution du nom de domaine www.gstatic.com

13:08:53.672292 IP archlinux-matheo.43146 > _gateway.domain: 56214+ A? www.gstatic.com. (33)
13:08:53.681512 IP _gateway.domain > archlinux-matheo.43146: 56214 1/0/0 A 216.58.205.195 (49)

Connection TCP

13:08:55.426416 IP 194.214.237.10.36723 > archlinux-matheo.snapenetio: Flags [S], seq 2335130437, win 64240
13:08:55.426445 IP archlinux-matheo.snapenetio > 194.214.237.10.36723: Flags [S.], seq 3316363639, ack 2335130438, win 65160
13:08:55.462400 IP 194.214.237.10.36723 > archlinux-matheo.snapenetio: Flags [.], ack 1, win 502

Étapes du handshake TCP :

  1. [S] : SYN - Demande de connexion
  2. [S.] : SYN-ACK - Acceptation de la connexion
  3. [.] : ACK - Confirmation

Échange de données TLS/HTTPS

13:08:55.465585 IP 194.214.237.10.36723 > archlinux-matheo.snapenetio: Flags [P.], seq 1:224, ack 1, win 502, length 223
13:08:55.466156 IP archlinux-matheo.snapenetio > 194.214.237.10.36723: Flags [P.], seq 1:1034, ack 224, win 64, length 1033
13:08:56.093808 IP archlinux-matheo.42356 > 82.221.107.34.bc.googleusercontent.com.http: Flags [.], ack 3134761312, win 63 13:08:56.105422 IP 82.221.107.34.bc.googleusercontent.com.http > archlinux-matheo.42356: Flags [.], ack 1, win 1050

Exercice 3

Implémentez un sniffer affichant un message à chaque paquet reçu.

// #include [...]
 
int main(int argc, char *argv[]) {
  if (argc < 2) {
    fprintf(stderr, "Usage: %s <interface>\n", argv[0]);
    return EXIT_FAILURE;
  }
  const char *ifname = argv[1];
 
  // Création d'une socket RAW pour capturer tous les paquets
  int sock_rec = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
  if (sock_rec == -1) {
    perror("socket(AF_PACKET)");
    return EXIT_FAILURE;
  }
 
  // Configuration de l'interface réseau
  struct sockaddr_ll sll;
  memset(&sll, 0, sizeof(sll));
  struct ifreq ifr;
  memset(&ifr, 0, sizeof(ifr));
  strncpy(ifr.ifr_name, ifname, IFNAMSIZ - 1);
  
  if (ioctl(sock_rec, SIOCGIFINDEX, &ifr) == -1) {
    perror("ioctl(SIOCGIFINDEX)");
    close(sock_rec);
    return EXIT_FAILURE;
  }
  
  sll.sll_family = AF_PACKET;
  sll.sll_ifindex = ifr.ifr_ifindex;
  sll.sll_protocol = htons(ETH_P_ALL);
 
  // Liaison de la socket à l'interface
  if (bind(sock_rec, (struct sockaddr *)&sll, sizeof(sll)) == -1) {
    perror("bind");
    close(sock_rec);
    return EXIT_FAILURE;
  }
 
  printf("Sniffer démarré sur l'interface %s...\n\n", ifname);
 
  // Buffer pour stocker les paquets reçus
  unsigned char buffer[65536];
  int packet_count = 0;
 
  // Boucle de réception
  while (1) {
    struct sockaddr_ll from;
    socklen_t fromlen = sizeof(from);
    
    ssize_t rec_len = recvfrom(sock_rec, buffer, sizeof(buffer), 0,
                               (struct sockaddr *)&from, &fromlen);
    if (rec_len < 0) {
      perror("recvfrom");
      close(sock_rec);
      return EXIT_FAILURE;
    }
 
    packet_count++;
    printf("Paquet #%d reçu : %zd octets\n", packet_count, rec_len);
  }
 
  close(sock_rec);
  return EXIT_SUCCESS;
}

1 Messages IP

Exercice 4

Ajoutez l’affichage d’informations de base sur les messages IP : temps de réception, source, destination, taille, type de protocole.

// Récupération du timestamp
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
char timestr[64];
struct tm *tm_info = localtime(&ts.tv_sec);
strftime(timestr, sizeof(timestr), "%Y-%m-%d %H:%M:%S", tm_info);
 
// Extraction et affichage des informations IP
struct iphdr *ip = (struct iphdr *)(buffer + ip_off);
char src_ip[INET_ADDRSTRLEN], dst_ip[INET_ADDRSTRLEN];
struct in_addr saddr, daddr;
saddr.s_addr = ip->saddr;
daddr.s_addr = ip->daddr;
inet_ntop(AF_INET, &saddr, src_ip, sizeof(src_ip));
inet_ntop(AF_INET, &daddr, dst_ip, sizeof(dst_ip));
 
printf(
    "[%s.%03ld] - IP: %s -> %s\n      protocol=%u\n      size=%zd octets",
    timestr, ts.tv_nsec / 1000000L, src_ip, dst_ip, ip->protocol, rec_len);
  • Utilisation de clock_gettime(CLOCK_REALTIME, &ts) pour obtenir l’horodatage
  • Conversion des adresses IP depuis le format réseau (ip->saddr, ip->daddr) vers des chaînes lisibles par les humains avec inet_ntop()
  • Affichage du protocole (ip->protocol) et de la taille totale du paquet

2 Messages ICMP

Exercice 5

Ajoutez la prise en compte des messages ICMP.

Pour la suite du TP, nous avons isolé la gestion et l’affichage de chaque protocole dans des procédures. Ainsi, dans la fonction main, nous dispatchons la gestion du paquet IP selon le protocole comme suit :

switch (ip->protocol) {
// [...] Autres protocoles
case IPPROTO_ICMP:
  dump_icmp(rec_len, buffer, transport_off);
  break;
}

Exemple du résultat de dump_icmp sur la sortie standard lors de l’exécution de la commande ping 192.168.1.254 -c 1 :

[2025-11-11 09:57:52.138] - 192.168.1.130	->	192.168.1.254		98 octets
		ICMP: type=8, code=0
[2025-11-11 09:57:52.142] - 192.168.1.254	->	192.168.1.130		98 octets
		ICMP: type=0, code=0

Exercice 6

Ajoutez la prise en charge des types de messages ICMP suivants : echo request, echo reply, time exceeded, port unreachable.

Ajout d’un switch dans la procédure dump_icmp, les type de message sont des macros qui proviennent de #include <netinet/ip_icmp.h> (voir [[#tp2---utilisations-de-sockets-de-type-raw|TP2 - Utilisations de sockets de type RAW]])

void dump_icmp(ssize_t rec_len, unsigned char *buffer, size_t transport_off) {
  if (rec_len < (ssize_t)(transport_off + sizeof(struct icmphdr))) {
    printf("entête icmp malformé\n");
    return;
  }
 
  struct icmphdr *icmp = (struct icmphdr *)(buffer + transport_off);
  printf("\t\tICMP: type=%u, code=%u\n", icmp->type, icmp->code);
 
  switch (icmp->type) {
  case ICMP_ECHOREPLY:
    printf("\t\tEcho Reply\n");
    break;
  case ICMP_DEST_UNREACH:
    printf("\t\tDestination Unreachable\n");
    break;
  case ICMP_ECHO:
    printf("\t\tEcho Request\n");
    break;
  case ICMP_TIME_EXCEEDED:
    printf("\t\tTime Exceeded\n");
    break;
  default:
    break;
  }
}

Exercice 7

Vérifiez que votre sniffer fonctionne en exécutant ping et traceroute sur une machine distante. Analysez les résultats.

Sortie standard après l’exécution de ping 192.168.1.254 -c 1 :

[2025-11-11 10:07:30.554] - 192.168.1.130	->	192.168.1.254		98 octets
		ICMP: type=8, code=0
		Echo Request
[2025-11-11 10:07:30.557] - 192.168.1.254	->	192.168.1.130		98 octets
		ICMP: type=0, code=0
		Echo Reply

Sortie standard après l’exécution de :

tracepath 8.8.8.8
 1?: [LOCALHOST]                      pmtu 1500
 1:  _gateway                                              3.597ms 
 1:  _gateway                                              2.170ms 
 2:  no reply
 3:  212-195-252-172.abo.bbox.fr                          27.791ms 
 4:  no reply
 5:  212.194.170.0                                        13.802ms asymm  7 
 6:  no reply
[2025-11-11 10:12:44.605] - 192.168.1.254	->	192.168.1.130		590 octets
		ICMP: type=11, code=0
		Time Exceeded
[2025-11-11 10:12:44.608] - 192.168.1.254	->	192.168.1.130		590 octets
		ICMP: type=11, code=0
		Time Exceeded
[2025-11-11 10:12:47.639] - 212.195.252.172	->	192.168.1.130		70 octets
		ICMP: type=11, code=0
		Time Exceeded
[2025-11-11 10:12:48.016] - 192.168.1.130	->	192.168.1.254		179 octets
		ICMP: type=3, code=3
		Destination Unreachable
[2025-11-11 10:12:48.369] - 192.168.1.130	->	192.168.1.254		123 octets
		ICMP: type=3, code=3
		Destination Unreachable
[2025-11-11 10:12:48.384] - 192.168.1.130	->	192.168.1.254		135 octets
		ICMP: type=3, code=3
		Destination Unreachable
[2025-11-11 10:12:50.668] - 212.194.170.0	->	192.168.1.130		70 octets
		ICMP: type=11, code=0
		Time Exceeded
[2025-11-11 10:13:01.359] - 192.168.1.130	->	192.168.1.254		133 octets
		ICMP: type=3, code=3
		Destination Unreachable

3 Messages UDP

Exercice 8

Ajoutez la prise en compte des messages UDP. Vous afficherez le numéro de port source et le numéro de port destination.

Ajout du cas protocole UDP à notre switch dans la fonction main :

case IPPROTO_UDP:
  dump_udp(rec_len, buffer, transport_off);
  break;

Exemple de message sur la sortie standard :

[2025-11-11 10:16:07.869] - 192.168.1.254	->	192.168.1.130		95 octets
	UDP: ports 53 --> 37165 

Ce paquet utilise le port 53 donc probablement un paquet DNS…

Exercice 9

Exécutez à nouveau traceroute sur une machine distante et analysez le résultat.

4 Messages TCP

Exercice 10

Ajoutez la prise en compte des messages TCP. Vous afficherez le numéro de port source et le numéro de port destination.

Ajout du cas protocole TCP à notre switch et de la procédure dump_tcp :

switch (ip->protocol) {
case IPPROTO_TCP:
  dump_tcp(rec_len, buffer, transport_off);
  break;
case IPPROTO_UDP:
  dump_udp(rec_len, buffer, transport_off);
  break;
case IPPROTO_ICMP:
  dump_icmp(rec_len, buffer, transport_off);
  break;
}
void dump_tcp(ssize_t rec_len, unsigned char *buffer, size_t transport_off) {
  if (rec_len < (ssize_t)(transport_off + sizeof(struct tcphdr))) {
    printf("entête TCP malformé \n");
    return;
  }
  struct tcphdr *tcp = (struct tcphdr *)(buffer + transport_off);
 
  unsigned short sport = ntohs(tcp->source);
  unsigned short dport = ntohs(tcp->dest);
  size_t payload_off = transport_off + tcp->doff * 4;
  ssize_t payload_len = rec_len - payload_off;
 
  printf("\tTCP: ports %u -> %u \n", sport, dport);
 
  char *payload = (char *)(buffer + payload_off);
  // [...]
}

Exemple de messages sur la sortie standard lors d’une communication TCP :

[2025-11-11 10:39:54.448] - 194.214.237.10	->	192.168.1.130		843 octets
	TCP: ports 35324 -> 22000 
[2025-11-11 10:39:54.449] - 194.214.237.10	->	192.168.1.130		66 octets
	TCP: ports 35324 -> 22000 
[2025-11-11 10:39:54.966] - 140.82.121.5	->	192.168.1.130		105 octets
	TCP: ports 443 -> 43266 
[2025-11-11 10:39:54.966] - 192.168.1.130	->	140.82.121.5		66 octets
	TCP: ports 43266 -> 443 
[2025-11-11 10:39:54.966] - 192.168.1.130	->	140.82.121.5		105 octets
	TCP: ports 43266 -> 443 
[2025-11-11 10:39:54.966] - 192.168.1.130	->	140.82.121.5		90 octets
	TCP: ports 43266 -> 443 
[2025-11-11 10:39:54.981] - 140.82.121.5	->	192.168.1.130		66 octets
	TCP: ports 443 -> 43266

Exercice 11

Ajoutez la prise en compte des messages HTTP. Votre sniffer affichera la page demandée dès qu’il aura recueilli suffisamment d’information.

Dans la procédure dump_tcp nous ajoutons un ensemble de conditions basé sur les numéros de port afin d’identifier les protocoles utilisés. Dans un premier temps, HTTP et HTTPS :

  char *payload = (char *)(buffer + payload_off);
  if (sport == 443 || sport == 80) {
    dump_http(payload, payload_len);
  }

La procédure dump_http :

void dump_http(char *payload, ssize_t payload_len) {
  if (strstr(payload, "GET ") || strstr(payload, "POST ") ||
      strstr(payload, "HTTP/1.1") || strstr(payload, "HTTP/1.0")) {
    fwrite(payload, 1, (payload_len < 512 ? payload_len : 512), stdout);
    printf("\n");
  }
}

On se limite au 512 premiers octets de la payload.

Exemple en exécutant la commande curl http://httpforever.com :

  1. Résolution DNS (les résultats montrés ici prennent en compte les changement effectué dans l’Exercice 14)
[2025-11-11 12:21:28.765] - 192.168.1.130	->	192.168.1.254		75 octets
	UDP: ports 56139 -> 53 
    DNS Query: httpforever.com    DNS Answer: Type A, Class IN
[2025-11-11 12:21:28.775] - 192.168.1.254	->	192.168.1.130		91 octets
	UDP: ports 53 -> 56139 
    DNS Query: httpforever.com    DNS Answer: Type A, Class IN
      IP Address: 146.190.62.39

[2025-11-11 12:21:28.775] - 192.168.1.130	->	192.168.1.254		75 octets
	UDP: ports 35499 -> 53 
    DNS Query: httpforever.com    DNS Answer: Type AAAA, Class IN
[2025-11-11 12:21:28.788] - 192.168.1.254	->	192.168.1.130		103 octets
	UDP: ports 53 -> 35499 
    DNS Query: httpforever.com    DNS Answer: Type AAAA, Class IN
      IPv6 Address: 2604:a880:4:1d0::1f1:2000

On constate que l’on a pu résoudre deux adresse IP. Pour simplifier, nous allons forcer l’IPv4 en la passant directement à la commande curl (sinon par défaut, curl va poursuivre avec l’IPv6).

  1. Requête HTTP
[2025-11-11 12:15:27.324] - 192.168.1.130	->	146.190.62.39		143 octets
	TCP: ports 44534 -> 80 
GET / HTTP/1.1
Host: 146.190.62.39
User-Agent: curl/8.17.0
Accept: */*
  1. Réponse HTTP
[2025-11-11 12:15:27.532] - 146.190.62.39	->	192.168.1.130		2962 octets
	TCP: ports 80 -> 44534 
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Tue, 11 Nov 2025 11:20:27 GMT
Content-Type: text/html
Content-Length: 5124
Last-Modified: Wed, 22 Mar 2023 14:54:48 GMT
Connection: keep-alive
ETag: "641b16b8-1404"
Referrer-Policy: strict-origin-when-cross-origin
X-Content-Type-Options: nosniff
Feature-Policy: accelerometer 'none'; camera 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; payment 'none'; usb 'none'
Content-Security-Policy: default-src 'self'; script-src cdnjs.cloudflare.com 'self' 'report-sha256'; style-src cdnjs.cloudflare.com 'self' fonts.googleapis.com 'unsafe-inline'; font-src fonts.googleapis.com fonts.gstatic.com cdnjs.cloudflare.com; frame-ancestors 'none'; report-uri https://scotthelme.report-uri.com/r/d/csp/enforce
Accept-Ranges: bytes

<!DOCTYPE HTML>
<html>
	<head>
		<title>HTTP Forever</title>
		<meta http-equiv="content-type" content="text/html; charset=utf-8" />
		<meta name="description" content="A site that will always be

Exercice 12

Ajoutez la prise en compte des messages SMTP. Votre sniffer affichera l’adresse email source et les adresses email destinations dès qu’il aura recueilli suffisamment d’information.

De la même manière, voici la procédure dump_smtp qui est appelé selon la condition suivante :

if (sport == 25 || dport == 25 || sport == 587 || dport == 587) {
  dump_smtp(payload, payload_len);
}
void dump_smtp(char *payload, ssize_t payload_len) {
  if (strstr(payload, "MAIL FROM:") || strstr(payload, "RCPT TO:")) {
    char *from = strstr(payload, "MAIL FROM:");
    if (from) {
      printf("    Source email: ");
      while (*from && *from != '<')
        from++;
      if (*from == '<')
        from++;
      while (*from && *from != '>')
        putchar(*from++);
      printf("\n");
    }
    char *to = strstr(payload, "RCPT TO:");
    if (to) {
      printf("    Destination email: ");
      while (*to && *to != '<')
        to++;
      if (*to == '<')
        to++;
      while (*to && *to != '>')
        putchar(*to++);
      printf("\n");
    }
  }
}

Exercice 13

Ajoutez la prise en compte des messages POP3. Votre sniffer affichera le nom d’utilisateur source dès qu’il aura recueilli suffisamment d’information.

if (sport == 110 || dport == 110) {
  dump_pop3(payload, payload_len);
}
void dump_pop3(char *payload, ssize_t payload_len) {
  if (strstr(payload, "USER ")) {
    return;
  }
  char *u = strstr(payload, "USER ");
  u += 5;
  printf("  POP3: username=");
  while (*u && *u != '\r' && *u != '\n')
    putchar(*u++);
  printf("\n");
}

Bien sur, pour tester ces deux protocoles, nous avons besoin de l’accès à un serveur mail donc nous ne sommes pas en mesure de fournir un exemple de ces deux derniers messages.

Exercice 14

Ajoutez la prise en compte des messages DNS. Votre sniffer affichera le nom compris dans la requête faite et l’adresse IP comprise dans la réponse, dès qu’il aura recueilli suffisamment d’information. 2

On notera que les paquets DNS peuvent utiliser les deux protocoles de transport (TPC ou UDP). On retrouve donc l’appel de dump_dns à deux endroits : dump_tcp et dump_udp.

if (sport == 53 || dport == 53) {
  dump_dns(payload, payload_len);
}
void dump_dns(char *payload, ssize_t payload_len) {
  if (payload_len < 12)
    return;
  char *qname = payload + 12;
  printf("    DNS Query: ");
  char *p = qname;
  while (*p) {
    int len = *p++;
    for (int i = 0; i < len; i++)
      putchar(*p++);
    if (*p)
      putchar('.');
  }
 
  // Print the DNS answer
  printf("    DNS Answer: ");
  p += 1; // skip null byte
  if (p + 4 > payload + payload_len)
    return;
  unsigned short qtype = ntohs(*(unsigned short *)p);
  p += 2;
  unsigned short qclass = ntohs(*(unsigned short *)p);
  p += 2;
  if (qtype == 1) {
    printf("Type A, ");
  } else if (qtype == 28) {
    printf("Type AAAA, ");
  } else {
    printf("Type %u, ", qtype);
  }
  if (qclass == 1) {
    printf("Class IN\n");
  } else {
    printf("Class %u\n", qclass);
  }
 
  char *ans = p;
  ans += 12; // skip TTL and data length
  if (ans + 4 > payload + payload_len)
    return;
  if (qtype == 1) {
    struct in_addr addr;
    memcpy(&addr, ans, 4);
    char ip_str[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &addr, ip_str, sizeof(ip_str));
    printf("      IP Address: %s\n", ip_str);
  } else if (qtype == 28) {
    if (ans + 16 > payload + payload_len)
      return;
    struct in6_addr addr6;
    memcpy(&addr6, ans, 16);
    char ip6_str[INET6_ADDRSTRLEN];
    inet_ntop(AF_INET6, &addr6, ip6_str, sizeof(ip6_str));
    printf("      IPv6 Address: %s\n", ip6_str);
  }
 
  printf("\n");
}

Exemple d’affichage d’un paquet DNS :

[2025-11-11 10:54:05.830] - 192.168.1.254	->	192.168.1.130		103 octets
	UDP: ports 53 -> 56530 
    DNS Query: mail.google.com    DNS Answer: Type AAAA, Class IN
      IPv6 Address: 2a00:1450:4006:811::2005

Améliorations

Support des IPv6 dans l’affichage des paquets DNS

Nous avons ajouté le support des IPv6, car la plupart des noms de domaines courant pointent vers des IPv6. Pour ce faire, nous avons traité les IP selon le type d’enregistrement DNS : A (qtype == 1) pour les IPv4 et AAAA (qtype == 28) pour les IPv6. Voir le code présenté dans l’Exercice 14.