Articles
Source : NoFutur, Noway, NoRoute, Jokers, Hacker2020, Phrack, libertad, lotfree, Wikipédia, etc..
Retour à la liste des articles

Remote Procedure Call (RPC)

L'appel de fonction est un mécanisme très bien connu des programmeurs, d'où l'idée de programmer des applications distribuées en appelant des fonctions qui sont situées sur une machine distante, ce qui explique le nom de "Remote Procedure Call".

Vous pouvez utiliser les RPC pour toutes sortes d'applications distribuées, par exemple l'utilisation d'un ordinateur très puissant pour des calculs intensifs (décryptage de données, calculs numériques, ...). Cet ordinateur sera donc le serveur. Un autre ordinateur sera le client et appelera la procédure distante pour commander des calculs au serveur et récupérer le résultat.

L'idée de Remote Procedure Call n'est pas nouvelle. De fait, de nombreux systèmes RPC sont disponibles (et incompatibles entre eux). Nous allons étudier ici le système de Sun Microsystems ou "Sun RPC" qui est devenu un standard de fait car ses spécifications sont dans le domaine public. Ce système a été développé pour servir de base au système NFS (Network File System) de Sun, largement utilisé sous Linux. Vous trouverez dans votre distribution préférée tous les fichiers (entêtes, fonctions RPC) et outils nécessaires, ne serait-ce que pour pouvoir recompiler client et serveur NFS.

Principe de fonctionnement

Le système RPC s'efforce de maintenir le plus possible la sémantique habituelle des appels de fonction, autrement dit tout doit être le plus transparent possible pour le programmeur. Pour que cela ressemble à un appel de fonction local, il existe dans le programme client une fonction locale qui a le même nom que la fonction distante et qui, en réalité, appelle d'autres fonctions de la bibliothèque RPC qui prennent en charge les connexions réseaux, le passage des paramètres et le retour des résultats. De même, côté serveur il suffira (à quelques exceptions près) d'écrire une fonction comme on en écrit tous les jours, un processus se chargeant d'attendre les connexions clientes et d'appeler votre fonction avec les bons paramètres. Il se chargera ensuite de renvoyer les résultats. Les fonctions qui prennent en charge les connexions réseaux sont des "stub". Il faut donc écrire un stub client et un stub serveur en plus du programme client et de la fonction distante.

Le travail nécessaire à la construction des stubs client et serveur sera automatisé grâce au programme rpcgen qui produira du code C qu'il suffira alors de compiler. Il ne restera plus qu'à écrire le programme client qui appelle la fonction et en utilise le résultat (par exemple en l'affichant) et la fonction elle-même.

Interface

Pour comprendre le fonctionnement des RPC, nous allons donc écrire, à titre d'exemple, un programme simple mais néanmoins complet . Il y a en fait deux programmes, un programme client et un programme serveur. La fonction distante prendra deux nombres en paramètres et renverra leur somme ainsi qu'un code d'erreur indiquant s'il y a eu un overflow ou non. Le travail commence donc par la définition de l'interface, celle-ci étant écrite en utilisant l'IDL (Interface Definition Language) du système RPC (proche du C).

struct data {
  unsigned int arg1;
  unsigned int arg2;
};

typedef struct data data;

struct reponse {
  unsigned int somme;
  int errno;
};

typedef struct reponse reponse;

program CALCUL{
  version VERSION_UN{
    void CALCUL_NULL(void) = 0;
    reponse CALCUL_ADDITION(data) = 1;
  } = 1;
} = 0x20000001;

Cette définition est enregistrée dans le fichier calcul.x . Ce fichier décrit notre programme et les fonctions qu'il contient. La définition parle d'elle-même. Notre programme s'appelle CALCUL et dans sa version VERSION_UN contient deux procédures: CALCUL_NULL et CALCUL_ADDITION.

Le programme a un nom (ici CALCUL) et un numéro (ici 0x20000001), ce numéro identifie ce programme de manière unique dans le monde. C'est pratique pour des programmes comme le daemon NFS. Dans notre cas, le numéro est choisi dans l'intervalle allant de 0x20000000 à 0x3FFFFFFF, réservé pour les utilisateurs et ne risque pas d'entrer en conflit avec des programmes tournant déjà sur votre machine.

Ensuite, pour un programme donné, on peut avoir plusieurs versions, ceci afin de pouvoir offrir de nouvelles fonctions dans la nouvelle version tout en conservant les versions précédentes (pour les anciens programmes clients). Ici, nous avons donc une version appelée VERSION_UN et qui a pour numéro 1.

Vient ensuite, la liste des procédures que le serveur implémente. Chaque procédure a un nom et un numéro. Une procédure de numéro 0 et qui ne fait rien (ici CALCUL_NULL) est toujours requise. Ceci afin de tester si le système marche (une sorte de ping en quelque sorte). On peut l'appeler afin de vérifier que le réseau fonctionne et que le serveur tourne. La seconde procédure décrite est notre procédure d'addition. Elle prend un argument de type data et renvoie un argument de type reponse. Le système RPC n'autorise qu'un seul argument en paramètre et un seul en retour, pour passer plusieurs arguments (dans notre cas les deux nombres à additionner), on utilise donc une structure. De même pour renvoyer plusieurs valeurs (dans notre cas le résultat de l'addition et un code d'erreur), on utilise une structure .

Ce fichier de définition est ensuite traité par l'utilitaire rpcgen (RPC program generator), il suffit de taper sur la ligne de commande:

rpcgen -a calcul.x

L'option -a permet de produire un squelette pour notre programme client (calcul_client.c) et un squelette pour la fonction distante (calcul_server.c). Avec ou sans cette option, les fichiers suivants sont également produits: calcul.h (entête), calcul_clnt.c (stub client), calcul_svc.c (stub serveur) et calcul_xdr.c (routines XDR).

Le format XDR (eXternal Data Representation) définit les types utilisés pour l'échange de variables entre le client et le serveur. Il est possible que les processus serveur et client ne tournent pas sur la même plateforme, il est donc indispensable de parler une langue commune. Ainsi ce format définit précisément un codage pour les entiers, les flottants... Les structures plus complexes utilisent les types de base. Ainsi, les types que nous avons nous-mêmes définis nécessitent un filtre XDR, c'est le rôle des fonctions définies dans le fichier calcul_xdr.c, à compiler puis à lier avec le client et le serveur. La compilation peut être faite maintenant (c'est déjà ça) et produit un calcul_xdr.o :

gcc -c calcul_xdr.c

Les stubs client et serveur sont complets et peuvent déjà être compilés. La seule connaissance de l'interface suffit à rpcgen pour les générer. Donc, nous pouvons compiler pour produire les fichiers calcul_clnt.o et calcul_svc.o :

gcc -c calcul_clnt.c
gcc -c calcul_svc.c

Processus serveur

Ceci étant fait, écrivons maintenant la fonction distante qui effectue réellement le travail. Grâce à l'option -a de rpcgen nous avons le squelette de fonction suivant dans le fichier calcul_server.c:

/*
 * This is sample code generated by rpcgen.
 * These are only templates and you can use them
 * as a guideline for developing your own functions.
 */

#include "calcul.h"

void * 
calcul_null_1_svc(void *argp, struct svc_req *rqstp)
{
  
  static char* result;
  
  /*
   * insert server code here
   */
  
  return((void*) &result);
}

reponse * 
calcul_addition_1_svc(data *argp, struct svc_req *rqstp)
{
  
  static reponse  result;
  
  /*
   * insert server code here
   */
  
  return(&result);
}

Après examen du fichier produit par rpcgen, quelques explications s'imposent, car on peut remarquer quelques différences par rapport à notre définition. Les fonctions ont deux arguments, le deuxième n'a pas d'utilité pour notre exemple. Au lieu de récupérer une variable du type demandé dans la définition, on doit en fait passer par un pointeur sur cette variable (ceci évite une copie des arguments et donc il y a un gain de temps et de mémoire). Pour le retour, c'est également un pointeur qui est renvoyé. Comme on utilise des pointeurs, la variable dans laquelle on met la valeur de retour est déclarée 'static' car il faut bien évidemment passer l'adresse d'une variable qui existe encore après la fin de la fonction.

Comme vous le voyez, il n'y a pas de fonction main. La fonction main est située dans le stub serveur. Le stub serveur s'occupe de recevoir et de "dispatcher" les appels aux fonctions adéquates. Notre seul travail est d'écrire les fonctions. Donc, après modifications, le fichier calcul_server.c devient:

#include "calcul.h"

void * 
calcul_null_1_svc(void *argp, struct svc_req *rqstp)
{
  static char* result;
  /* Ne rien faire */
  return((void*) &result);
}

reponse * 
calcul_addition_1_svc(data *argp, struct svc_req *rqstp)
{
  static reponse  result;
  unsigned int max;

  result.errno = 0; /* Pas d'erreur */

  /* Prend le max */
  max = argp->arg1 > argp->arg2 ? argp->arg1 : argp->arg2;

  /* On additionne */
  result.somme = argp->arg1 + argp->arg2; 

  /* Overflow ? */
  if ( result.somme < max ) {
    result.errno = 1;
  }
    
  return(&result);
}

Nous pouvons alors le compiler en faisant:

gcc -c calcul_server.c

Puis pour obtenir le programme serveur complet, il faut lier calcul_svc.o, calcul_server.o et calcul_xdr.o ensemble:

gcc -o server calcul_svc.o calcul_server.o calcul_xdr.o

A ce stade, nous avons terminé la partie serveur de notre application. On peut alors démarrer le serveur, puis utiliser rpcinfo pour vérifier qu'il tourne:

> ./server &
[2] 209
> rpcinfo -p
   program vers proto   port
    100000    2   tcp    111  rpcbind
    100000    2   udp    111  rpcbind
    100005    1   udp    673  mountd
    100005    2   udp    673  mountd
    100005    1   tcp    676  mountd
    100005    2   tcp    676  mountd
    100003    2   udp   2049  nfs
    100003    2   tcp   2049  nfs
 536870913    1   udp    897
 536870913    1   tcp    899
> rpcinfo -u localhost 536870913
program 536870913 version 1 ready and waiting

L'option -p de rpcinfo permet de connaître la liste des programmes RPC actuellement enregistrés sur la machine. Pour chaque programme, on obtient le numéro de version, le protocole et le port utilisés. Veuillez noter, que les numéros de programmes sont affichés en décimal . Sachant que 536870913 est l'équivalent décimal de 0x20000001, tout va bien notre programme est bien enregistré. L'option -u quant à elle, permet de tester le programme indiqué en appelant sa procédure 0 (rappelez vous qu'il est obligatoire d'avoir au moins une procédure de numéro 0). Pour plus d'information, vous pouvez consulter la page man de rpcinfo.

Processus client

Là encore, pour simplifier, on utilise le squelette que nous a fourni rpcgen en produisant le fichier calcul_client.c :

/*
 * This is sample code generated by rpcgen.
 * These are only templates and you can use them
 * as a guideline for developing your own functions.
 */

#include "calcul.h"


void
calcul_1( char* host )
{
  CLIENT *clnt;
  void  *result_1;
  char*  calcul_null_1_arg;
  reponse  *result_2;
  data  calcul_addition_1_arg;
  clnt = clnt_create(host, CALCUL, VERSION_UN, "udp");
  if (clnt == NULL) {
    clnt_pcreateerror(host);
    exit(1);
  }
  result_1 = calcul_null_1((void*)&calcul_null_1_arg, clnt);
  if (result_1 == NULL) {
    clnt_perror(clnt, "call failed:");
  }
  result_2 = calcul_addition_1(&calcul_addition_1_arg, clnt);
  if (result_2 == NULL) {
    clnt_perror(clnt, "call failed:");
  }
  clnt_destroy( clnt );
}


main( int argc, char* argv[] )
{
  char *host;

  if(argc < 2) {
    printf("usage: %s server_host
", argv[0]);
    exit(1);
  }
  host = argv[1];
  calcul_1( host );
}

Des variables sont déclarées pour les arguments et les valeurs de retour. Comme vous pouvez le constater le programme squelette généré comprend un appel à chacune des fonctions définies dans l'interface (ici CALCUL_NULL et CALCUL_ADDITION). On peut remarquer que chaque appel est suivi d'un test qui détecte les erreurs de niveau "RPC" (serveur ne répondant pas, machine inexistante, ...). L'erreur éventuelle est alors explicitée par la fonction clnt_perror(). Quand une erreur de niveau "RPC" se produit, le pointeur renvoyé est à NULL. Dans vos fonctions, vous ne devez donc pas mettre le pointeur à NULL pour spécifier une erreur. Pour un niveau d'erreur autre que RPC, vous pouvez utiliser une valeur particulière de la valeur de retour (comme un nombre négatif par exemple) et renvoyer tout à fait normalement le pointeur sur cette variable. Dans l'exemple, le choix a été fait d'utiliser une deuxième variable (champ errno de la structure reponse) pour spécifier s'il y a eu une erreur ou non (car l'utilisation de valeurs particulières est discutable).

Pour faire un vrai programme, il nous faut donner des valeurs aux paramètres et il faut utiliser effectivement les résultats des appels distants (par exemple en les affichant à l'écran). C'est ce que nous faisons avec notre programme client (dans lequel il n'y a volontairement pas d'appel à la procédure CALCUL_NULL) :

#include <limits.h>
#include "calcul.h"

CLIENT *clnt;

void
test_addition (uint param1, uint param2)
{	
  reponse  *resultat;
  data  parametre;

  /* 1. Preparer les arguments */
  
  parametre.arg1 = param1;
  parametre.arg2 = param2; 
  printf("Appel de la fonction CALCUL_ADDITION avec les parametres: %u et %u 
", parametre.arg1,parametre.arg2);

  /* 2. Appel de la fonction distante */
  
  resultat = calcul_addition_1 (¶metre, clnt);
  if (resultat == (reponse *) NULL) {
    clnt_perror (clnt, "call failed");
    clnt_destroy (clnt);
    exit(EXIT_FAILURE);
  }
  else if ( resultat->errno == 0 ) {
    printf("Le resultat de l'addition est: %u 

",resultat->somme);
  } else {
    printf("La fonction distante ne peut faire l'addition a cause d'un overflow 

");
  }
  
}

int
main (int argc, char *argv[])
{
  char *host;
  
  if (argc < 2) {
    printf ("usage: %s server_host
", argv[0]);
    exit (1);
  }
  host = argv[1];
  
  clnt = clnt_create (host, CALCUL, VERSION_UN, "udp");
  if (clnt == NULL) {
    clnt_pcreateerror (host);
    exit (1);
  }
  
  test_addition ( UINT_MAX - 15, 10 );
  test_addition ( UINT_MAX, 10 );
  
  clnt_destroy (clnt);
  exit(EXIT_SUCCESS);
}

Nous pouvons alors compiler puis lier avec calcul_clnt.o et calcul_xdr.o pour produire le client. Enfin, on peut tester le résultat final (en ayant démarré le processus serveur avant).

> gcc -c calcul_client.c
> gcc -o client calcul_client.o calcul_clnt.o calcul_xdr.o

> ./client localhost
Appel de la fonction CALCUL_ADDITION avec les parametres: 4294967280 et 10 
Le resultat de l'addition est: 4294967290 

Appel de la fonction CALCUL_ADDITION avec les parametres: 4294967295 et 10 
La fonction distante ne peut faire l'addition a cause d'un overflow 
> 

Le client prend en paramètre le nom de la machine serveur (ici localhost car le processus serveur a été démarré sur la même machine). Le nom peut être court (machine) pour une machine du même domaine que le client ou complet (machine.domaine.com).

Conclusion

Avec l'aide de la bibliothèque de fonctions RPC (clnt_create, clnt_destroy, ....) et des outils comme rpcgen et rpcinfo, il a été très facile de construire une application simple mais toutefois représentative du mécanisme RPC. Pour aller plus loin, vous pouvez consulter la page man de rpcgen ainsi que le RFC 1057.

Aujourd'hui des systèmes plus perfectionnés ont fait leur apparition comme par exemple CORBA. Certains diront peut-être que les RPC, sont en quelque sorte l'ancêtre de CORBA dans lequel il ne s'agit plus de fonctions distantes mais d'objets (dans le sens de la programmation orienté objet) distants.


François CREVOLA


-- Avant d'être disponible sur la présente page, cet article est d'abord paru dans GNU/Linux & Hurd Magazine France (numéro 20, SEPTEMBRE 2000) que je remercie de m'avoir ouvert ses pages --
Cet article est sous licence FDL (Free Documentation Licence)
Livre:

WatchFire


Vidéo:
Cours Sécurité:
  • Définitions Niveau : Débutant (Français)
    tout les mots que vous voulez savoir mais que vous n'osez demander.
  • Histoire Niveau : Débutant (Français)
    un peu d'histoire pour ceux qui aiment
  • Cours Metasploit Niveau : Intermediaire (Français)
    Le Framework Metasploit est un environnement complet pour écrire, tester et utiliser des exploits. Cet environnement fournit une plateforme solide pour les tests d'intrusion, le développement de shellcodes, et la recherche de vulnérabilités. La majorité du Framework est composée de code orienté objet Perl, avec des composants optionnels écrits en C, assembleur, et Python.
  • hackers_guide Niveau : Avancé (Anglais)
    Se placer du point de vue de l'attaquant pour améliorer sa propre sécurité. - Telecharger le rar: hackers_guide.rar
  • Reseaux de neuronnes Niveau : Avancé (Français)
    Les objectifs, l'histoire, les applications. Le modèle d'un neurone, fonction de transfert, architecture de réseau. Les outils mathématiques : algère linéaire, espace vectoriel, produit scalair, norme, orthogonalité, transformation linéaire. Les apprentissages : supervisé, non-supervisé et autres. Le perceptron multicouches, les nuées dynamiques (K-means, fuzzy k-means...). Les réseaux de Kohonen. Reseaux GNG, Architecture ART. ACP et apprentissage hebbien. - Telecharger le zip: RNF
  • Crackage et durcissement de mots de passesNiveau : Débutant (Français)
  • Securiser un forum PhpBBNiveau : intermediaire (Français)
Tutoriaux
Tutoriaux Failles Web
Divers