Différences entre versions de « Php socket »

De The Linux Craftsman
Aller à la navigation Aller à la recherche
 
(11 versions intermédiaires par le même utilisateur non affichées)
Ligne 83 : Ligne 83 :
 
<?php
 
<?php
 
if (($socket = socket_create ( AF_INET, SOCK_STREAM, SOL_TCP )) == FALSE) {
 
if (($socket = socket_create ( AF_INET, SOCK_STREAM, SOL_TCP )) == FALSE) {
echo "socket_create_listen() a échoué : " . socket_strerror ( socket_last_error ($socket) ) . "\n";
+
echo "socket_create() a échoué : " . socket_strerror ( socket_last_error ($socket) ) . "\n";
 
exit ( 1 );
 
exit ( 1 );
 
}
 
}
Ligne 297 : Ligne 297 :
  
 
= Socket UDP (Datagram) =
 
= Socket UDP (Datagram) =
 +
[[Fichier:Warning-icon.png|20px]] Sur Windows remplacez ''MSG_EOF'' par ''0'' !
 
==Partie serveur==
 
==Partie serveur==
 
Pour créer une socket UDP, il n'y a qu'une seule façon, en utilisant [http://php.net/manual/fr/function.socket-create.php socket_create] et [http://php.net/manual/fr/function.socket-bind.php socket_bind].
 
Pour créer une socket UDP, il n'y a qu'une seule façon, en utilisant [http://php.net/manual/fr/function.socket-create.php socket_create] et [http://php.net/manual/fr/function.socket-bind.php socket_bind].
Ligne 320 : Ligne 321 :
 
Ici la fonction [http://php.net/manual/fr/function.socket-recvfrom.php socket_recvfrom] permet de récupérer ce qui arrive de la socket UDP et [http://php.net/manual/fr/function.socket-sendto.php socket_sendto] permet de le renvoyer au client.
 
Ici la fonction [http://php.net/manual/fr/function.socket-recvfrom.php socket_recvfrom] permet de récupérer ce qui arrive de la socket UDP et [http://php.net/manual/fr/function.socket-sendto.php socket_sendto] permet de le renvoyer au client.
 
== Partie cliente ==
 
== Partie cliente ==
Il ne reste plus qu'à écrire un programme ''client'' qui va lire l'entrée console et l'envoyé au serveur en UDP :
+
Il ne reste plus qu'à écrire un programme ''client'' qui va lire l'entrée console et l'envoyer au serveur en UDP :
 
<source lang="php" style="font-size:110%">
 
<source lang="php" style="font-size:110%">
 
<?php
 
<?php
 
$socket = socket_create ( AF_INET, SOCK_DGRAM, SOL_UDP );
 
$socket = socket_create ( AF_INET, SOCK_DGRAM, SOL_UDP );
 +
$srv_port = '1234';
 
echo "UDP client (type 'quit' to stop client)\n";
 
echo "UDP client (type 'quit' to stop client)\n";
 
$quit = false;
 
$quit = false;
Ligne 332 : Ligne 334 :
 
$quit = true;
 
$quit = true;
 
}else{
 
}else{
socket_sendto($socket, $buf, strlen($buf), MSG_EOF, "127.0.0.1", 1234);
+
socket_sendto($socket, $buf, strlen($buf), MSG_EOF, "127.0.0.1", $srv_port);
socket_recvfrom($socket, $srvBuf, 2048, MSG_OOB, $srv_ip, $srv_port);
+
socket_recvfrom($socket, $srvBuf, 2048, MSG_OOB, "127.0.0.1", $srv_port);
 
echo $srvBuf."\n";
 
echo $srvBuf."\n";
 
}
 
}
Ligne 340 : Ligne 342 :
 
?>
 
?>
 
</source>
 
</source>
 +
 
==Niveau réseau==
 
==Niveau réseau==
 
On peut observer la mise en place de notre socket grâce à la commande :  
 
On peut observer la mise en place de notre socket grâce à la commande :  
Ligne 378 : Ligne 381 :
 
</pre>
 
</pre>
  
Nous allons commencer par créer une classe ''Header'' qui va contenir les différents type de données ainsi que la méthode pour fabriquer l'entête nécessaire poru la fonction ''unpack''. Ci dessous le contenu du fichier ''Header.class.php'' :
+
Nous allons commencer par créer une classe ''Header'' qui va contenir les différents type de données ainsi que la méthode pour fabriquer l'entête nécessaire pour la fonction ''unpack''. Ci dessous le contenu du fichier ''Header.class.php'' :
 
<source lang="php" style="font-size:110%">
 
<source lang="php" style="font-size:110%">
 
<?php
 
<?php
Ligne 423 : Ligne 426 :
 
<source lang="php" style="font-size:110%">
 
<source lang="php" style="font-size:110%">
 
<?php
 
<?php
include 'Header.class.php'
+
include 'Header.class.php';
 
class IpHeader {
 
class IpHeader {
 
/**
 
/**
Ligne 536 : Ligne 539 :
 
* chaque champ commence par sa taille (C = 8bits, n = 16bits, etc...) ;
 
* chaque champ commence par sa taille (C = 8bits, n = 16bits, etc...) ;
 
* chaque champ finit par son nom.
 
* chaque champ finit par son nom.
 +
 
=== Écoute du trafic ===
 
=== Écoute du trafic ===
  
 
On va maintenant créer un programme qui va ouvrir une socket et utiliser les classes précédentes pour capturer les paquets IP :  
 
On va maintenant créer un programme qui va ouvrir une socket et utiliser les classes précédentes pour capturer les paquets IP :  
 
<source lang="php" style="font-size:110%">
 
<source lang="php" style="font-size:110%">
<?php
 
 
#!/usr/bin/php
 
#!/usr/bin/php
 
<?php
 
<?php
Ligne 711 : Ligne 714 :
 
const DATA = 'data';
 
const DATA = 'data';
 
/**
 
/**
* Champs typés de l'entête IP
+
* Champs typés de l'entête TCP
 
*/
 
*/
 
const UNPACK_HEADER = array (
 
const UNPACK_HEADER = array (
Ligne 879 : Ligne 882 :
 
const DATA = 'data';
 
const DATA = 'data';
 
/**
 
/**
* Champs typés de l'entête IP
+
* Champs typés de l'entête UDP
 
*/
 
*/
 
const UNPACK_HEADER = array (
 
const UNPACK_HEADER = array (
Ligne 912 : Ligne 915 :
 
?>
 
?>
 
</source>
 
</source>
 
+
Il faut modifier notre programme pour ajouter la ligne suivante :
Il faut modifier notre programme pour ajouter les lignes suivantes :
 
 
* dans l'entête du programme :
 
* dans l'entête du programme :
 
<source lang="php" style="font-size:110%">
 
<source lang="php" style="font-size:110%">
Ligne 970 : Ligne 972 :
  
 
=== Paquet ICMP ===
 
=== Paquet ICMP ===
On va procéder de la même façon qu'avec l'entête UDP, en analysant les champs qui composent l'entête UDP :  
+
On va procéder de la même façon qu'avec l'entête UDP, en analysant les champs qui composent l'entête ICMP :  
 
<pre>
 
<pre>
 
  0                  1                  2                  3
 
  0                  1                  2                  3
Ligne 1 101 : Ligne 1 103 :
 
en
 
en
 
<source lang="php" style="font-size:110%">
 
<source lang="php" style="font-size:110%">
 +
//print_r($packet);
 
if ($packet [IpHeader::PROTO] == 1) {
 
if ($packet [IpHeader::PROTO] == 1) {
 
// Paquet ICMP
 
// Paquet ICMP
Ligne 1 114 : Ligne 1 117 :
 
---------- ICMP END ----------
 
---------- ICMP END ----------
 
</pre>
 
</pre>
 
== Envoie d'un paquet ==
 

Version actuelle datée du 22 avril 2019 à 15:41

Introduction

Dans le contexte des logiciels, on peut le traduire par « connecteur réseau » ou « interface de connexion ».

Apparu dans les systèmes UNIX, un socket est un élément logiciel qui est aujourd’hui répandu dans la plupart des systèmes d’exploitation. Il s’agit d’une interface logicielle avec les services du système d’exploitation, grâce à laquelle un développeur exploitera facilement et de manière uniforme les services d’un protocole réseau.

[...]

Il s’agit d’un modèle permettant la communication inter processus (IPC - Inter Process Communication) afin de permettre à divers processus de communiquer aussi bien sur une même machine qu’à travers un réseau TCP/IP.

[...]

On distingue ainsi deux modes de communication :

  • Le mode connecté [...] utilisant le protocole TCP. [...]
  • Le mode non connecté [...] utilisant le protocole UDP.

[...]

Les sockets se situent entre la couche transport et les couches applicatives du modèle OSI (protocoles UDP ou TCP utilisant IP / ARP).

Wikipedia

Mise en œuvre

Les socket vont nous permettre de faire communiquer deux processus entre eux grâce à un protocole, généralement un processus appelé serveur et l'autre processus appelé client. C'est pourquoi on parle souvent de protocole client / serveur.

Tout est détaillé sur le site php.net mais il est important de s'attarder sur certains points.

Socket TCP (Stream)

Établissement d'une connexion

Pour créer une socket, il y a deux façons, soit on utilise :

La première méthode va permettre de choisir l'adresse sur laquelle on va placer la socket en écoute ainsi que le type de socket. La deuxième méthode permet uniquement de créer des socket AF_INET (IPv4) de type SOCK_STREAM (TCP).

Si l'objectif est de créer un démon qui écoute sur toutes les interfaces en TCP, la deuxième méthode est plus appropriée.
Au contraire, si l'objectif est de faire de l'IPC, une socket UDP ou même brute (OSI 3), il vaut mieux opter pour la première méthode.

Voici un exemple de socket TCP avec la première méthode.
Ce socket écoute sur le port 1234 de l'interface 127.0.0.1 en TCP :

Voici un exemple de socket TCP avec la deuxième méthode.
Ce socket écoute sur le port 1234 de toutes les interfaces en TCP :

<?php
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($socket, "127.0.0.1", 1234);
socket_listen($socket);
while($c = socket_accept($socket)) {
	/* Traiter la requête entrante */
}
socket_close($socket);
?>
<?php
$socket = socket_create_listen(1234);
while($c = socket_accept($socket)) {
	/* Traiter la requête entrante */
}
socket_close($socket);
?>

Que se passe t-il si le port est déjà utilisé, si les droits ne permettent pas de binder la socket, si le type de socket demandée n'existe pas ? Un erreur survient et regardons maintenant comment l'attraper !

Gestion des erreurs

Pour attraper les erreurs, il faut tester le retour des fonctions énoncées précédemment. Si celui-ci est à FALSE une erreur est survenue et on peut récupérer son code et même une explication textuelle.

Voici un exemple avec la première méthode.

Voici un exemple avec la deuxième méthode.

<?php
if (($socket = socket_create ( AF_INET, SOCK_STREAM, SOL_TCP )) == FALSE) {
	echo "socket_create() a échoué : " . socket_strerror ( socket_last_error ($socket) ) . "\n";
	exit ( 1 );
}
if(socket_bind ( $socket, "127.0.0.1", 1234 )==FALSE){
	echo "socket_bind() a échoué : " . socket_strerror ( socket_last_error ($socket) ) . "\n";
	exit ( 1 );
}
if(socket_listen ( $socket )==FALSE){
	echo "socket_listen() a échoué : " . socket_strerror ( socket_last_error ($socket) ) . "\n";
	exit ( 1 );
}
while ( $c = socket_accept ( $socket ) ) {
	/* Traiter la requête entrante */
}
socket_close ( $socket );
?>
<?php
if (($socket = socket_create_listen ( 1234 )) == FALSE) {
	echo "socket_create_listen() a échoué : " . socket_strerror ( socket_last_error ($socket) ) . "\n";
	exit ( 1 );
}
while ( $c = socket_accept ( $socket ) ) {
	/* Traiter la requête entrante */
}
socket_close ( $socket );
?>

Cela donnera le résultat suivant :

PHP Warning:  socket_create_listen(): unable to bind to given address [98]: Address already in use in /root/workspace/Sockets/src/socket.php on line 2
socket_create() a échoué : raison : Address already in use

Pour enlever le PHP warning il suffit d'ajouter un '@' devant la fonction qui génère le warning

$socket = socket_create_listen ( 1234 )

devient

$socket = @socket_create_listen ( 1234 )

Traitement des connexions entrantes

On va maintenant s'intéresser à la récupération des connexions entrante. Pour cela, nous allons créer un petit serveur echo.

<?php
if (($socket = socket_create_listen ( 1234 )) == FALSE) {
	echo "socket_create_listen() a échoué : " . socket_strerror ( socket_last_error ($socket) ) . "\n";
	exit ( 1 );
}
while ( $c = socket_accept ( $socket ) ) {
	while ( $c !== FALSE ) {
		if ($buf = socket_read ( $c, 2048 )) {
			socket_write ( $c, "You said : ".$buf );
		}
	}
}
socket_close ( $socket );
?>

Ce qui nous donne :

côté serveur :

coté client, en utilisant la commande telnet :

# php -f socket.php

# telnet 127.0.01 1234
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
coucou
You said : coucou

Lorsque l'on termine le processus serveur en envoyant un SIGINT (ctrl + c), le client reçoit le message Connection closed by foreign host. et le serveur laisse la socket en TIME_WAIT. C'est un comportement tout à fait normale car la connexion, à l'initiative du client, et terminée par le serveur et le système attends un certain temps (d’où le nom de TIME_WAIT) pour laisser aux derniers segments TCP le temps d'arriver.

Tout cela nous importe peu, sauf que lors du prochain lancement du script :

# php -f test.php
PHP Warning:  socket_create_listen(): unable to bind to given address [98]: Address already in use in /root/workspace/Sockets/src/socket.php on line 2

Effectivement, lorsque l'on regarde qui occupe le port 1234 :

# netstat -atnp | grep 1234
tcp        0      0 127.0.0.1:1234              127.0.0.1:47408             TIME_WAIT   -  

C'est bien le système qui laisse la socket ouverte un certain temps, même si personne n'écoute (- à la place du nom du processus).

Nous allons maintenant modifier les options de la socket pour éviter cet effet.

Modification des options de socket

Pour régler le problème précédent, deux choix s'offre à nous :

  • SO_REUSEADDR : option qui permet, lorsqu'elle est mise à 1, de binder de nouveau une socket à un port en TIME_WAIT en assumant qu'il n'y ai aucun paquet en transit ;
  • SO_LINGER : option qui, lorsqu'elle est mise à 0, initie une fermeture anormale de la socket. Ce phénomène s'appelle en anglais slamming the connection shut ou en français raccrocher brutalement la connexion.

Nous allons préférer la première option qui est plus propre. Cependant, l'exemple précédent qui utilise socket_create_listen ne permet pas de modifier l'option avant l'étape de bind puisque les trois étapes sont regroupées. Nous allons donc basculer sur la première méthode...

if (($socket = socket_create ( AF_INET, SOCK_STREAM, SOL_TCP )) == FALSE) {
	echo "socket_create_listen() a échoué : " . socket_strerror ( socket_last_error ($socket) ) . "\n";
	exit ( 1 );
}
// Modification de l'option SO_REUSEADDR à la valeur 1 !
if (!socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1)) {
	echo 'Impossible de définir l\'option du socket : '. socket_strerror(socket_last_error($socket)) . "\n";
	exit ( 1 );
}
if(socket_bind ( $socket, "127.0.0.1", 1234 )==FALSE){
	echo "socket_bind() a échoué : " . socket_strerror ( socket_last_error ($socket) ) . "\n";
	exit ( 1 );
}
if(socket_listen ( $socket )==FALSE){
	echo "socket_listen() a échoué : " . socket_strerror ( socket_last_error ($socket) ) . "\n";
	exit ( 1 );
}
while ( $c = socket_accept ( $socket ) ) {
	while ( $c !== FALSE ) {
		if ($buf = socket_read ( $c, 2048 )) {
			socket_write ( $c, "You said : " . $buf );
		}
	}
}
socket_close ( $socket );

On peut maintenant relancer le script sans problème !

Traitement multi-client

Le problème de notre serveur echo est qu'il ne peut pas traiter plus de un client à la fois. Ce qui offre un intérêt limité pour un serveur... Nous allons remédier à cela en passant les sockets en mode non bloquant.

Les fonctions socket_accept et socket_read vont rendre la main en renvoyant FALSE si aucune connexion n'est disponible ou si le tampon d'entrée est vide.

// Passage en mode non bloquant de la socket du serveur
socket_set_nonblock ( $socket );
$clients = array ();
while ( TRUE ) {
	if ($c = socket_accept ( $socket )) {
		// Passage en mode non bloquant de la socket du client
		socket_set_nonblock ( $c );
		// Ajout de la socket cliente au tableau
		$clients [] = $c;
	}
	// On répond au clients qui ont envoyés un message
	for($i = 0; $i < sizeof ( $clients ); $i ++) {
		$c = $clients [$i];
		if ($buf = socket_read ( $c, 2048 )) {
			socket_write ( $c, "You said : " . $buf );
		}
	}
	// On efface les sockets fermées
	for($i = 0; $i < sizeof ( $clients ); $i ++) {
		$c = $clients [$i];
		if ($c == FALSE) {
			$clients = array_splice ( $clients, $i, 1 );
		}
	}
}
socket_close ( $socket );

On constate que plusieurs clients peuvent maintenant utiliser le serveur echo.

Consommation excessive

Cependant le script prend un peu trop de ressource :

# top | grep php
  7844 root      20   0  264m 9356 5740 R 98.1  0.9   0:07.72 php

Il faut avouer, 98% de CPU pour un script PHP de ce calibre... c'est un peu exagéré ! Le faite d'avoir passé les sockets en mode non bloquant, revient à faire une boucle itérative sans temps d'arrêt, ce qui à pour conséquence de consommer la quasi totalité du CPU.

Il suffit d'ajouter un temps de repos dans notre boucle :

while ( TRUE ) {
	...
	usleep(500);
}

On constate que cette petite sieste de 500 µs fonctionne parfaitement :

# top | grep php
  7874 root      20   0  264m 9364 5748 S  2.3  0.9   0:00.07 php

L'occupation du CPU est passée de 98% à 2.3% !

Niveau réseau

On peut observer la mise en place de notre socket grâce à la commande :

# tcpdump -i lo -nn -vv -w socket_tcp.pkt tcp and port 1234

L'option -w permet d'enregistrer les trames dans un fichier qui peut être lu avec Wireshark.

On peut constater :

  • l'établissement de la connexion (SYN / ACK+SYN / ACK)

Php socket tcp establishment.png

  • l'échange des données (PSH / ACK)
    • les trames
      Php socket tcp stream.png
    • le contenu
      Php socket tcp stream data.png
  • la fin de la connexion (FIN / ACK)

Php socket tcp end.png

Socket UDP (Datagram)

Warning-icon.png Sur Windows remplacez MSG_EOF par 0 !

Partie serveur

Pour créer une socket UDP, il n'y a qu'une seule façon, en utilisant socket_create et socket_bind.

Voici un exemple de serveur qui utilise une socket UDP qui écoute sur le port 1234 de toutes les interfaces :

<?php
$socket = socket_create ( AF_INET, SOCK_DGRAM, SOL_UDP );
socket_bind ( $socket, "127.0.0.1", 1234 );
socket_listen ( $socket );
while ( TRUE ) {
	$byteReceived = socket_recvfrom( $socket, $buf, 2048, MSG_DONTWAIT, $remote_ip, $remote_port);
	if ( $byteReceived > 0 ) {
		echo "New datagram received from $remote_ip:$remote_port : $buf";
		$buf = "You said : " . $buf;
		socket_sendto ( $socket, $buf, strlen($buf), MSG_EOF, $remote_ip, $remote_port );
	}
}
socket_close ( $socket );
?>

Ici la fonction socket_recvfrom permet de récupérer ce qui arrive de la socket UDP et socket_sendto permet de le renvoyer au client.

Partie cliente

Il ne reste plus qu'à écrire un programme client qui va lire l'entrée console et l'envoyer au serveur en UDP :

<?php
$socket = socket_create ( AF_INET, SOCK_DGRAM, SOL_UDP );
$srv_port = '1234';
echo "UDP client (type 'quit' to stop client)\n";
$quit = false;
while (!$quit){
	echo "Say something :\t";
	$buf = readline();
	if($buf == "quit"){
		$quit = true;
	}else{
		socket_sendto($socket, $buf, strlen($buf), MSG_EOF, "127.0.0.1", $srv_port);
		socket_recvfrom($socket, $srvBuf, 2048, MSG_OOB, "127.0.0.1", $srv_port);
		echo $srvBuf."\n";
	}
}
socket_close($socket);
?>

Niveau réseau

On peut observer la mise en place de notre socket grâce à la commande :

# tcpdump -i lo -w socket_udp.pkt udp and port 1234

On observe très bien les deux datagrammes échangés :
Php socket udp exchange.png
Qui contiennent les données suivantes :
Php socket udp data.png

Mode RAW

Le mode RAW permet de tomber au niveau 3 OSI. Cela permet de pouvoir décortiquer les paquets IP ou ICMP qui arrivent sur la machine ou bien de forger un paquet avant de l'envoyer.

Lecture d'un paquet

Le principe est simple :

  • on reçoit les données binaire de la socket en mode RAW ;
  • on créer un tableau représentant l'entête (longueur des champs) ;
  • on utilise la fonction unpack pour déconditionner les données selon le tableau entête précédant ;
  • on utilise le tableau pour faire un traitement.

Entête IP

Pour pouvoir lire un paquet IP, il faut d'abord connaître l'entête IP :

    0                   1                   2                   3   
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |Version|  IHL  |Type of Service|          Total Length         |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |         Identification        |Flags|      Fragment Offset    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |  Time to Live |    Protocol   |         Header Checksum       |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                       Source Address                          |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Destination Address                        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Options                    |    Padding    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Nous allons commencer par créer une classe Header qui va contenir les différents type de données ainsi que la méthode pour fabriquer l'entête nécessaire pour la fonction unpack. Ci dessous le contenu du fichier Header.class.php :

<?php
class Headers {
	/**
	 * Charactère non signé (8 bits)
	 */
	const TYPE_UINT8 = 'C';
	/**
	 * Entier court non signé (16 bits)
	 */
	const TYPE_UINT16 = 'n';
	/**
	 * Entier court non signé (32 bits)
	 */
	const TYPE_UINT32 = 'N';
	/**
	 * Chaîne hexadécimale (Big endian)
	 */
	const TYPE_HEX = 'H';
	/**
	 * Taille variable (jusqu'à la fin du paquet / datagramme)
	 */
	const TYPE_VARIABLE_LENGTH = '*';
	/**
	 * Retourne la chaîne prête pour la fonction unpack
	 *
	 * @return string
	 */
	public static function getUnPackHeader($header) {
		$len = sizeof ( $header );
		$str = "";
		for($i = 0; $i < $len; $i += 2) {
			$str .= $header [$i] . $header [$i + 1] . '/';
		}
		return $str;
	}
}
?>

On va maintenant créer la classe IpHeader qui représente l'entête IP. Ci-dessous le contenu du fichier IpHeader.class.php :

<?php
include 'Header.class.php';
class IpHeader {
	/**
	 * Version et longeur (4 + 4 bits)
	 */
	const VER_LEN = 'ip_ver_len';
	/**
	 * Type Of Service (8 bits)
	 */
	const TOS = 'tos';
	/**
	 * Longeur totale (16 bits)
	 */
	const TOT_LEN = 'tot_len';
	/**
	 * Identifiant de fragment (16 bits)
	 */
	const IDENT = 'identification';
	/**
	 * Indicateur de fragmentation et taille des fragements (3 + 13 bits)
	 */
	const INDICATOR_FRAGMENT_OFFSET = 'indic_frag_offset';
	/**
	 * Time To Live (16 bits)
	 */
	const TTL = 'ttl';
	/**
	 * Protocole (8 bits)
	 */
	const PROTO = 'protocol';
	/**
	 * Checksum (16 bits)
	 */
	const CHECKSUM = 'checksum';
	/**
	 * Adresse source (32 bits)
	 */
	const SRC_ADDR = 'src_addr';
	/**
	 * Adresse destination (32 bits)
	 */
	const DST_ADDR = 'dst_addr';
	/**
	 * Données du paquet IP
	 */
	const PAYLOAD = 'payload';
	/**
	 * Champs typés de l'entête IP
	 */
	const UNPACK_HEADER = array (
			Headers::TYPE_UINT8,
			self::VER_LEN,
			Headers::TYPE_UINT8,
			self::TOS,
			Headers::TYPE_UINT16,
			self::TOT_LEN,
			Headers::TYPE_UINT16,
			self::IDENT,
			Headers::TYPE_UINT16,
			self::INDICATOR_FRAGMENT_OFFSET,
			Headers::TYPE_UINT8,
			self::TTL,
			Headers::TYPE_UINT8,
			self::PROTO,
			Headers::TYPE_UINT16,
			self::CHECKSUM,
			Headers::TYPE_UINT32,
			self::SRC_ADDR,
			Headers::TYPE_UINT32,
			self::DST_ADDR 
	);
	/**
	 * Permet de récupérer l'entête IP depuis les données binaires d'une socket RAW
	 * 
	 * @param string $raw        	
	 * @return array
	 */
	public static function getHeader($raw) {
		$packet = unpack ( self::getUnPackHeader (), $raw );
		return array (
				self::VER_LEN => ($packet [self::VER_LEN] >> 4),
				self::VER_LEN => ($packet [self::VER_LEN] & 0x0F),
				self::TOS => $packet [self::TOS],
				self::TOT_LEN => $packet [self::TOT_LEN],
				self::IDENT => $packet [self::IDENT],
				self::INDICATOR_FRAGMENT_OFFSET => $packet [self::INDICATOR_FRAGMENT_OFFSET],
				self::TTL => $packet [self::TTL],
				self::PROTO => $packet [self::PROTO],
				self::CHECKSUM => $packet [self::CHECKSUM],
				self::SRC_ADDR => long2ip ( $packet [self::SRC_ADDR] ),
				self::DST_ADDR => long2ip ( $packet [self::DST_ADDR] ),
				self::PAYLOAD => $packet [self::PAYLOAD] 
		);
	}
	/**
	 * Retourne la chaîne prête pour la fonction unpack
	 *
	 * @return string
	 */
	private static function getUnPackHeader() {
		return Headers::getUnPackHeader ( self::UNPACK_HEADER ) . Headers::TYPE_HEX . Headers::TYPE_VARIABLE_LENGTH . self::PAYLOAD;
	}
}
?>

Voila le genre de chaîne que la fonction unpack prend en paramètre :

Cip_ver_len/Ctos/ntot_len/nidentification/nindic_frag_offset/Cttl/Cprotocol/nchecksum/Nsrc_addr/Ndst_addr/H*payloadCip_ver_len/Ctos/ntot_len/nidentification/nindic_frag_offset/Cttl/Cprotocol/nchecksum/Nsrc_addr/Ndst_addr/H*payload

On peut remarquer que :

  • les champs sont séparés par le caractère / ;
  • chaque champ commence par sa taille (C = 8bits, n = 16bits, etc...) ;
  • chaque champ finit par son nom.

Écoute du trafic

On va maintenant créer un programme qui va ouvrir une socket et utiliser les classes précédentes pour capturer les paquets IP :

#!/usr/bin/php
<?php
include 'Headers.class.php';
include 'IpHeader.class.php';

// Socket TCP
if (($socket = @socket_create ( AF_INET, SOCK_RAW, SOL_TCP )) === FALSE) {
	echo "Could not create TCP socket !";
	exit ( 1 );
}
socket_set_nonblock ( $socket );
echo "Starting sniffing...\n";
while ( TRUE ) {
	if ((socket_recv ( $socket, $raw, 65536, 0 )) > 0) {
			$packet = IpHeader::getHeader ( $raw );
			print_r($packet);
	}
	usleep ( 100 );
}
?>

Lorsqu'on lance le script, on commence à voir les paquets entrant :

# ./socket.php
Starting sniffing...
Array
(
    [ip_ver_len] => 5
    [tos] => 0
    [tot_len] => 40
    [identification] => 18098
    [indic_frag_offset] => 16384
    [ttl] => 128
    [protocol] => 6
    [checksum] => 12290
    [src_addr] => 192.168.1.3
    [dst_addr] => 192.168.1.200
    [payload] => c0cd0016f829bd49a0453e1850103fa397600000
)
Array
(
    [ip_ver_len] => 5
    [tos] => 0
    [tot_len] => 40
    [identification] => 18099
    [indic_frag_offset] => 16384
    [ttl] => 128
    [protocol] => 6
    [checksum] => 12289
    [src_addr] => 192.168.1.3
    [dst_addr] => 192.168.1.200
    [payload] => c0cd0016f829bd49a0453f8c50103f4696490000
)

Si on regarde le numéro de protocole, 6 correspond à un segment TCP.

Segment TCP

Pour pouvoir lire une entête TCP, il faut d'abord connaître les champs qui la compose:

0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          Source Port          |       Destination Port        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                        Sequence Number                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Acknowledgment Number                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Data |           |U|A|P|R|S|F|                               |
| Offset| Reserved  |R|C|S|S|Y|I|            Window             |
|       |           |G|K|H|T|N|N|                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|           Checksum            |         Urgent Pointer        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Options                    |    Padding    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                             DATA                              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

On va maintenant créer la classe TcpHeader qui représente l'entête TCP. Ci-dessous le contenu du fichier TcpHeader.class.php :

<?php
class TcpHeader {
	
	/**
	 * Port source (16 bits)
	 */
	const SRC_PORT = 'src_port';
	/**
	 * Port destination (16 bits)
	 */
	const DST_PORT = 'dst_port';
	/**
	 * Numéro de séquence (32 bits)
	 */
	const SEQ_NUM = 'seq_num';
	/**
	 * Numéro d'acquittement (32 bits)
	 */
	const ACK_NUM = 'ack_num';
	/**
	 * Bits réservés (8 bits)
	 */
	const LENGTH = 'length';
	/**
	 * Bits réservés (8 bits)
	 */
	const LENGTH_RESERVED_ECN = 'lre';
	/**
	 * Flags TCP (8 bits)
	 */
	const FLAGS = 'flags';
	/**
	 * Flags TCP Explicit Congestion Notification (1 bits)
	 */
	const FLAG_ECN = 'ecn';
	/**
	 * Flags TCP Congestion Window Reduced (1 bits)
	 */
	const FLAG_CWR = 'cwr';
	/**
	 * Flags TCP Explicit Congestion Notification-Echo (1 bits)
	 */
	const FLAG_ECE = 'ece';
	/**
	 * Flags TCP Urgent (1 bits)
	 */
	const FLAG_URG = 'urg';
	/**
	 * Flags TCP Acknoledgment (1 bits)
	 */
	const FLAG_ACK = 'ack';
	/**
	 * Flags TCP Push (1 bits)
	 */
	const FLAG_PSH = 'psh';
	/**
	 * Flags TCP Reset (1 bits)
	 */
	const FLAG_RST = 'rst';
	/**
	 * Flags TCP Synchro (1 bits)
	 */
	const FLAG_SYN = 'syn';
	/**
	 * Flags TCP FIN (1 bits)
	 */
	const FLAG_FIN = 'fin';
	/**
	 * Taille de la fenêtre (16 bits)
	 */
	const WIN_SIZE = 'win_size';
	/**
	 * Cheksum (16 bits)
	 */
	const CHECKSUM = 'checksum';
	/**
	 * Pointeur de données urgente (16 bits)
	 */
	const URGENT = 'urgent';
	/**
	 * Option et Padding (32 bits)
	 */
	const OPT_PAD = 'opt_pad';
	/**
	 * Données du segment
	 */
	const DATA = 'data';
	/**
	 * Champs typés de l'entête TCP
	 */
	const UNPACK_HEADER = array (
			Headers::TYPE_UINT16,
			self::SRC_PORT,
			Headers::TYPE_UINT16,
			self::DST_PORT,
			Headers::TYPE_UINT32,
			self::SEQ_NUM,
			Headers::TYPE_UINT32,
			self::ACK_NUM,
			Headers::TYPE_UINT8,
			self::LENGTH_RESERVED_ECN,
			Headers::TYPE_UINT8,
			self::FLAGS,
			Headers::TYPE_UINT16,
			self::WIN_SIZE,
			Headers::TYPE_UINT16,
			self::CHECKSUM,
			Headers::TYPE_UINT16,
			self::URGENT 
	);
	/**
	 * Permet de récupérer l'entête TCP depuis les données IP (payload)
	 *
	 * @param binary $payload        	
	 * @return array
	 */
	public static function getHeader($payload) {
		$segment = unpack ( self::getUnPackHeader (), hex2bin ( $payload ) );
		$header = array (
				self::SRC_PORT => $segment [self::SRC_PORT],
				self::DST_PORT => $segment [self::DST_PORT],
				self::SEQ_NUM => $segment [self::SEQ_NUM],
				self::ACK_NUM => $segment [self::ACK_NUM],
				self::LENGTH => ($segment [self::LENGTH_RESERVED_ECN] >> 4),
				self::FLAGS => array (
						self::FLAG_ECN => ($segment [self::LENGTH_RESERVED_ECN] & 0x01),
						self::FLAG_CWR => (($segment [self::FLAGS] & 0x80) >> 7),
						self::FLAG_ECE => (($segment [self::FLAGS] & 0x40) >> 6),
						self::FLAG_URG => (($segment [self::FLAGS] & 0x20) >> 5),
						self::FLAG_ACK => (($segment [self::FLAGS] & 0x10) >> 4),
						self::FLAG_PSH => (($segment [self::FLAGS] & 0x08) >> 3),
						self::FLAG_RST => (($segment [self::FLAGS] & 0x04) >> 2),
						self::FLAG_SYN => (($segment [self::FLAGS] & 0x02) >> 1),
						self::FLAG_FIN => (($segment [self::FLAGS] & 0x01)) 
				),
				self::WIN_SIZE => $segment [self::WIN_SIZE],
				self::CHECKSUM => $segment [self::CHECKSUM] . ' [0x' . dechex ( $segment [self::CHECKSUM] ) . ']',
				self::DATA => $segment [self::DATA] 
		);
		if ($header [self::LENGTH] == 6) {
			$unpack_header = Headers::TYPE_UINT32 . self::OPT_PAD . Headers::TYPE_HEX . Headers::TYPE_VARIABLE_LENGTH . self::DATA;
			$segment = unpack ( Headers::getUnPackHeader ( $unpack_header ), hex2bin ( $header [self::DATA] ) );
			$header [self::OPT_PAD] = $segment [self::OPT_PAD];
			$header [self::DATA] = $segment [self::DATA];
		}
		return $header;
	}

	/**
	 * Retourne la chaîne prête pour la fonction unpack
	 *
	 * @return string
	 */
	private static function getUnPackHeader() {
		return Headers::getUnPackHeader ( self::UNPACK_HEADER ) . Headers::TYPE_HEX . Headers::TYPE_VARIABLE_LENGTH . self::DATA;
	}
}

Il faut modifier notre programme pour ajouter les lignes suivantes :

  • dans l'entête du programme :
include 'TcpHeader.class.php';
  • dans la boucle while :

en dessous de la ligne

print_r($packet);

On ajoute :

if ($packet [IpHeader::PROTO] == 6) {
	// Segment TCP
	$payload = TcpHeader::getHeader ( $packet [IpHeader::PAYLOAD] );
	print_r ( $payload );
}

Ce qui nous donne maintenant :

Array
(
    [ip_ver_len] => 5
    [tos] => 0
    [tot_len] => 40
    [identification] => 24491
    [indic_frag_offset] => 16384
    [ttl] => 128
    [protocol] => 6
    [checksum] => 5897
    [src_addr] => 192.168.1.3
    [dst_addr] => 192.168.1.200
    [payload] => c5e50016e0b34c4f34d276b4501040294d0a0000
)
Array
(
    [src_port] => 50661
    [dst_port] => 22
    [seq_num] => 3769846863
    [ack_num] => 886208180
    [length] => 5
    [flags] => Array
        (
            [ecn] => 0
            [cwr] => 0
            [ece] => 0
            [urg] => 0
            [ack] => 1
            [psh] => 0
            [rst] => 0
            [syn] => 0
            [fin] => 0
        )

    [win_size] => 16425
    [checksum] => 19722 [0x4d0a]
    [data] =>
)

Datagramme UDP

On va procéder de la même façon qu'avec l'entête TCP, en analysant les champs qui compose l'entête UDP :

0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          Source Port          |       Destination Port        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|            Length             |           Checksum            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                             DATA                              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

On va maintenant créer la classe UdpHeader qui représente l'entête UDP. Ci-dessous le contenu du fichier UdpHeader.class.php :

<?php
class UdpHeader {
	/**
	 * Port source (16 bits)
	 */
	const SRC_PORT = 'src_port';
	/**
	 * Port destination (16 bits)
	 */
	const DST_PORT = 'dst_port';
	/**
	 * Bits réservés (16 bits)
	 */
	const LENGTH = 'length';
	/**
	 * Cheksum (16 bits)
	 */
	const CHECKSUM = 'checksum';
	/**
	 * Données du segment
	 */
	const DATA = 'data';
	/**
	 * Champs typés de l'entête UDP
	 */
	const UNPACK_HEADER = array (
			Headers::TYPE_UINT16,
			self::SRC_PORT,
			Headers::TYPE_UINT16,
			self::DST_PORT,
			Headers::TYPE_UINT16,
			self::LENGTH,
			Headers::TYPE_UINT16,
			self::CHECKSUM 
	);
	/**
	 * Permet de récupérer l'entête UDP depuis les données IP (payload)
	 *
	 * @param binary $payload        	
	 * @return array
	 */
	public static function getHeader($payload) {
		return unpack ( self::getUnPackHeader (), hex2bin ( $payload ) );
	}

	/**
	 * Retourne la chaîne prête pour la fonction unpack
	 *
	 * @return string
	 */
	private static function getUnPackHeader() {
		return Headers::getUnPackHeader ( self::UNPACK_HEADER ) . Headers::TYPE_HEX . Headers::TYPE_VARIABLE_LENGTH . self::DATA;
	}
}
?>

Il faut modifier notre programme pour ajouter la ligne suivante :

  • dans l'entête du programme :
include 'UdpHeader.class.php';
  • il faut modifier la ligne suivant :
if (($socket = @socket_create ( AF_INET, SOCK_RAW, SOL_TCP )) === FALSE) {

en

if (($socket = @socket_create ( AF_INET, SOCK_RAW, SOL_UDP )) === FALSE) {
  • dans la boucle while :

Il faut modifier :

if ($packet [IpHeader::PROTO] == 6) {
	// Segment TCP
	$payload = TcpHeader::getHeader ( $packet [IpHeader::PAYLOAD] );
	print_r ( $payload );
}

en

if ($packet [IpHeader::PROTO] == 17) {
	// Datagram UDP
	$payload = UdpHeader::getHeader ( $packet [IpHeader::PAYLOAD] );
	print_r ( $payload );
}

Ce qui nous donne maintenant :

Array
(
    [ip_ver_len] => 5
    [tos] => 0
    [tot_len] => 56
    [identification] => 4046
    [indic_frag_offset] => 0
    [ttl] => 128
    [protocol] => 17
    [checksum] => 42699
    [src_addr] => 192.168.1.3
    [dst_addr] => 192.168.1.200
    [payload] => d16000350024823a1e020100000100000000000006636c7562696303636f6d0000010001
)
Array
(
    [src_port] => 53600
    [dst_port] => 53
    [length] => 36
    [checksum] => 33338
    [data] => 1e020100000100000000000006636c7562696303636f6d0000010001
)

Paquet ICMP

On va procéder de la même façon qu'avec l'entête UDP, en analysant les champs qui composent l'entête ICMP :

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     Type      |     Code      |          Checksum             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                             unused                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|      Internet Header + 64 bits of Original Data Datagram      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

On va maintenant créer la classe IcmpHeader qui représente l'entête ICMP. Ci-dessous le contenu du fichier IcmpHeader.class.php :

<?php
<?php
class IcmpHeader {
	/**
	 * Type de message ICMP
	 */
	const TYPE = 'type';
	/**
	 * Code de l'erreur
	 */
	const CODE = 'code';
	/**
	 * Checksum ICMP
	 */
	const CHECKSUM = 'checksum';
	/**
	 * Données du datagramme ICMP
	 */
	const DATA = 'data';
	/**
	 * Champs typés de l'entête ICMP
	 */
	const UNPACK_HEADER = array (
			Headers::TYPE_UINT8,
			self::TYPE,
			Headers::TYPE_UINT8,
			self::CODE,
			Headers::TYPE_UINT16,
			self::CHECKSUM 
	);
	const ICMP_CODE_LABEL = array (
			0 => "Echo reply",
			1 => "Reserved",
			2 => "Reserved",
			3 => "Unreachable",
			4 => "Source quench",
			5 => "Redirect",
			8 => "Echo request",
			11 => "Time elapsed",
			12 => "Wrong header",
			13 => "Time request",
			14 => "Time reply",
			15 => "IP request",
			16 => "IP reply",
			17 => "Subnet mask request",
			18 => "Subnet mask reply" 
	);
	/**
	 * Permet de récupérer l'entête ICMP depuis les données IP (payload)
	 *
	 * @param binary $payload        	
	 * @return array
	 */
	public static function getHeader($payload) {
		$packet = unpack ( self::getUnPackHeader (), hex2bin ( $payload ) );
		return array (
				self::TYPE => $packet [self::TYPE],
				self::CODE => $packet [self::CODE],
				self::CHECKSUM => $packet [self::CHECKSUM],
				self::DATA => $packet [self::DATA] 
		);
	}
	/**
	 * Affiche les ports et données dans la console
	 *
	 * @param array $packet
	 *        	le header IP
	 */
	public static function getPacketContent($packet) {
		$payload = self::getHeader($packet[IpHeader::PAYLOAD]);
		$str = "\n---------- ICMP BEGIN ----------\n";
		$str .= $packet [IpHeader::SRC_ADDR];
		$str .= " > ";
		$str .= $packet [IpHeader::DST_ADDR];
		$str .= "\nType : " . $payload [self::TYPE];
		$str .= " [" . self::ICMP_CODE_LABEL [$payload [self::TYPE]] . "]";
		$str .= "\nCode : " . $payload [self::CODE];
		$str .= "\n---------- ICMP END ----------\n";
		return $str;
	}
	/**
	 * Retourne la chaîne prête pour la fonction unpack
	 *
	 * @return string
	 */
	private static function getUnPackHeader() {
		return Headers::getUnPackHeader ( self::UNPACK_HEADER ) . Headers::TYPE_HEX . Headers::TYPE_VARIABLE_LENGTH . self::DATA;
	}
}
?>

Il faut modifier notre programme pour ajouter la ligne suivante :

  • dans l'entête du programme :
include 'IcmpHeader.class.php';
  • il faut modifier la ligne suivante :
if (($socket = @socket_create ( AF_INET, SOCK_RAW, SOL_UDP )) === FALSE) {

en

if (($socket = @socket_create ( AF_INET, SOCK_RAW, SOL_SOCKET )) === FALSE) {
  • dans la boucle while :

Il faut modifier :

print_r($packet);
if ($packet [IpHeader::PROTO] == 17) {
	// Paquet ICMP
	$payload = IcmpHeader::getHeader ( $packet [IpHeader::PAYLOAD] );
	print_r ( $payload );
}

en

//print_r($packet);
if ($packet [IpHeader::PROTO] == 1) {
	// Paquet ICMP
	echo IcmpHeader::getPacketContent($packet);
}

Ce qui nous donne maintenant :

---------- ICMP BEGIN ----------
192.168.1.3 > 192.168.1.200
Type : 8 [Echo request]
Code : 0
---------- ICMP END ----------