Introduction

Je vais ici vous parler de NGINX, le reverse proxy qui vous fait dire « отлично ! » et plus précisément de ses modules. Comme vous l’avez peut être constaté, nous nous intéressons plus que jamais en ce moment aux solutions défensives concernant l’hébergement.

Nous nous intéressons notamment de près à NGINX, le proxy venu de l’est. Ce dernier, outre ses performances et sa souplesse (dont on a déjà parlé ici),  propose un système de module flexible et performant. Là où le bât blesse, et la raison aussi de cet article c’est le manque de documentation concernant le développement des modules. Si on ne parle ni russe ni chinois, hormis un ou deux tutoriaux instructifs mais pas suffisants, on se retrouve très vite a grepper dans les .c pour comprendre comment fonctionne tel ou tel élément, notamment au niveau des API et des différentes structures employées.

L’objectif de ce post est donc de débroussailler les fondementaux du développement et du fonctionnement d’un module pour nginx.

Le fonctionnement global des modules sous NGINX

Les modules Nginx peuvent faire à peu près tout et n’importe quoi, grâce à un système de « phases ». En effet, lors du traitement d’une requête, celle-ci va passer par ce qu’Nginx appelle des phases, et il est possible d’enregistrer notre module pour une ou plusieurs phases :

  • POST_READ
  • SERVER_REWRITE
  • FIND_CONFIG
  • REWRITE
  • POST_REWRITE
  • PREACCESS
  • ACCESS
  • POST_ACCESS
  • TRY_FILES
  • CONTENT
  • LOG

    Je ne vais pas détailler ici l’intégralité des phases, la plupart d’entre elles sont relativement explicites.

Le module, une fois enregistré dans une des phases, et donc appelé par Nginx au moment convenu. Il pourra inspecter / modifier à la volée les entrées et sorties (comprendre la requête et la réponse), ou tout simplement appliquer des contrôles supplémentaires afin de décider s’il faut autoriser ou refuser l’exécution de la requêtes. Il me semble bon de préciser que, avec Nginx, la requête et la réponse sont stockées dans la même structure. De plus, ce système de phase est extrêmement riche, puisqu’il est possible de faire réappeler son module « a la volée ».

En effet, les modules, lorsque leur handler est appelé, peuvent, grâce au code de retour de ce dernier, préciser s’ils veulent être appelés à nouveau (NGX_AGAIN), s’ils ont fini leur travail sur cette phase (NGX_OK) etc. Mais leur code de retour peut aussi préciser l’issue de la requête, par exemple un handler qui renvoie NGX_HTTP_FORBIDEN provoquera l’arrêt de la requête et l’envoi d’un code de retour « 403 Forbiden ». Et la ou c’est encore plus fort, c’est que l’on peut, depuis le handler de son module, se réenregistrer, via d’autres systèmes de hooks.

Par exemple, mon module, qui fait de l’inspection de contenu, est positionné en phase « ACCESS » (C’est à ma connaissance la première phase viable pour placer un module d’inspection de contenu, autorisation par adresses IP etc.), et désire pouvoir inspecter, si la requête est de type POST/PUT, le contenu de cette requête.

La phase « ACCESS » est positionné très tôt dans la chaine des phases, et donc à ce moment, la donnée « POST/PUT » n’est pas encore accessible. Il suffit de faire un appel à « ngx_http_read_client_request_body(ngx_http_request *r, funcptr) » pour que notre fonction « funcptr » soit appelée lorsque l’intégralité des données POST/PUT seront lues. Il s’agit là d’un exemple trivial, mais qui illustre bien la souplesse de Nginx.

La gestion de la mémoire

Un des arguments imparable de Nginx est son footprint en mémoire : il est ridicule.
Il est ridicule grâce à l’utilisation d’une API extrêmement simple, mais à l’efficacité redoutable, reposant sur le concept de ‘pool’ de mémoire. Nginx met donc  à disposition des développeurs une série de « wrappers » aux fonctions d’allocation, comme par exemple « Ngx_pcalloc() » Ce wrapper de malloc prend en premier argument un « pool », suivi des arguments classiques de malloc.

C’est dans ce pool que seront effectuées les allocations mémoire, et Nginx se chargera de libérer la mémoire au moment venu. Ces fameux « pools » sont présents dans plusieurs structures, et l’utilisation de tel ou tel pool conditionnera donc le moment ou la mémoire sera libérée.

Par exemple, si dans notre cas on se servait du « pool » associé au ngx_http_request_t courant, cela provoquera, dès la fin du traitement de la requête, la libération de l’intégralité de nos allocations mémoire. En revanche, pour les elements de configuration, on utilisera le pool présent dans la structure ngx_conf_t *, ce qui nous garantira que notre mémoire allouée ne sera libérée qu’au shutdown du service.

On retrouve ce système astucieux de gestion de la mémoire à divers endroits, notamment dans les API fournies par Nginx pour gérer les tableaux, tables de hash etc.

Les structures de déclaration d’un module

La déclaration d’un module Nginx se fait au travers de différentes structures, qui sont bien souvent des tableaux contenant des pointeurs sur fonction (nos handlers) ou des pointeurs sur structure (les données de « configuration » du module) :

-    La configuration globale du module : Au travers d’un tableau de pointeur sur fonctions (ngx_http__ctx de type ngx_http_module_t), il est possible de préciser dans quelles étapes de configuration notre module doit être appelé (Attention : il ne s’agit pas des phases dont je vous parlais plus haut, mais réellement des étapes de configuration).

Il s’agit ici pour notre module de pouvoir s’insérer au bon endroit afin de pouvoir lire sa configuration. (préconfiguration, postconfiguration, main configuration, init configuration …). Globalement, lorsqu’il s’agit de lire des directives dans les fichiers de configuration de NGINX, l’emplacement « postconfiguration » semble suffisant.

static ngx_http_module_t ngx_http_dummy_module_ctx = {
NULL, /* preconfiguration */
ngx_http_dummy_init, /* postconfiguration */
NULL, /* create main configuration */
NULL, /* init main configuration */
NULL, /* create server configuration */
NULL, /* merge server configuration */
ngx_http_dummy_create_loc_conf, /* create location configuration */
ngx_http_dummy_merge_loc_conf /* merge location configuration */
};

PS : Les fonctions de ‘merge’ servent en fait au module de gérer correctement la configuration multi-niveau, dans le cas ou par exemple des directives du modules sont présentes dans un bloc ‘server’ et dans un bloc ‘location’ sous-jacent.

-    La définition du module : Au travers du tableau  ngx_http__module, de type ngx_module_t, il est possible de préciser un certain nombre d’éléments tels que le type de module, ses directives, ou encore son contexte.

Vous remarquerez l’utilisation du flag « NGX_HTTP_MODULE » eeeh oui, Nginx fait aussi proxy mail (pop/smtp/imap) !

-   Les « directives », au travers d’un tableau de structures ngx_command_t, on indique les directives que peut gérer notre module. On précise pour chacune d’entre elles :
o    Le nom de la commande, sous forme d’un ngx_string_t.
o    Différents flags relatifs à la commande, tels que les emplacements du fichier de configuration dans lesquels la commande est légitime, ou encore le nombre d’arguments que reçoit la directive (ici NGX_CONF_TAKE1 signifie que notre directive reçoit un seul argument).
o    La fonction en charge de parser les arguments de la directive. On peut soit se reposer sur des fonctions prédéfinies de NGINX, telles que ngx_conf_set_flag_num_slot (conversion d’une string en int), ngx_conf_set_str_slot (récupération d’une string passée en argument en format ngx_str_t), ou encore préciser sa « propre » fonction de parsing, comme ici.

static ngx_command_t ngx_http_dummy_commands[] = {


{ ngx_string(« denyRX »),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_HTTP_LMT_CONF
|NGX_CONF_TAKE1,
ngx_http_dummy_read_conf,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
ngx_null_command
};

Les structures propres à notre module

Ci-dessous une description succinte des principales structures propres à notre module.
La structure de configuration : Celle-ci est instanciée grâce à notre fonction « ngx_http_dummy_create_loc_conf » qui est référencée plus tôt. Son remplissage, en revanche est effectué par notre fonction « ngx_http_dummy_read_conf » référencée dans la structure relative au directives de configuration que gère notre module.

typedef struct
{
ngx_array_t *foobar;
} ngx_http_dummy_loc_conf_t;


On peut, à tout moment dans le code, récupérer un pointeur sur cette structure via un appel à « ngx_http_get_module_loc_conf(…) »
La structure de contexte : Celle-ci est relative au contexte, donc en général la requête sur laquelle on travaille. Bien souvent (du moins c’est mon cas), on crée cette structure lors du premier appel à notre handler.

Cette structure permettra de stocker toutes les données relatives à une requête (son état, son suivi etc.). Une fois la structure allouée, on l’enregistre en tant que structure de contexte :
ngx_http_set_ctx(r, ctx, ngx_http_dummy_module);

Et on peut aussi la récupérer ultérieurement :
ngx_http_get_module_ctx(r, ngx_http_dummy_module);
A noter que l’on peut récupérer/modifier la structure de contexte d’un autre module si l’on veut, ce qui permet de mettre en place un certain nombre de hacks, et permet de faciliter la communication et l’interaction entre les modules ;)

Voila pour cette entrée en matière, il y aura surement des posts à suivre sur le sujet, donc « stay tunned for moar fun »

Quelques liens forts instructifs :