Je vais vous donner un secret : c'est mon hamster qui fait tout le coding. Je suis juste un moyen pour mon hamster, une façade pour le reste du monde. Et donc, si il y a des bugs, ne vous plaignez pas à moi. Mettez ça sur le dos du poilu à la place.
iptables fournit simplement un tableau de règles en mémoire (d'où le nom iptables), et des informations comme quel paquet à partir de quel hook va où ? Après qu'une table est enregistrée, le userspace peut lire et remplacer sont contenu en utilisant les fonctions getsockopt() et setsockopt().
iptables ne s'enregistre avec aucun des hooks de netfilter, il s'appuie sur d'autres modules pour faire ça, et passe les paquets de manière appropriée. Un module doit enregistrer le hook netfilter et ip_tables séparément, et fournir un mécanisme pour appeler ip_tables quand le hook est atteint.
Pour la convenance, la même structure de données est utilisée pour représenter une règle en userspace et à l'intérieur du kernel (bien que quelques champs seulement sont utilisés dans le kernel).
Chaque règle contient les parties suivantes :
La nature variable des règles donne beaucoup de flexibilité pour les extensions, comme nous allons le voir, spécialement comme chaque ``target'' ou chaque ``match'' peut porter une taille arbitraire de données. Cependant, ceci a quelques désavantages : on a besoin de faire attention à l'alignement. On fait ça en s'assurant que les structures `ipt_entry', `ipt_entry_match' et `ipt_entry_target' sont de taille conventionnelle, et que les tailles de toutes les données sont arrondies vers le haut jusqu'à l'alignement maximum de la machine, en utilisant la macro IPT_ALIGN().
La `struct ipt_entry' a les champs suivants:
`struct ipt_entry_match' et `struct ipt_entry_target' sont très similaires, du fait qu'elles contiennent toutes les deux un champs (IPT_ALIGNé) représentant la longueur totale (respectivement `match_size' et `target_size') et une `union' contenant le nom du match ou de la target (pour le userspace), et un pointeur (pour le kernel).
A cause de la nature légèrement ... ``bidouille'' de la structure de données des règles, des fonctions d'aide sont fournies :
Cette fonction `inline' retourne un pointeur vers la target de la règle.
Cette macro appelle la fonction donnée pour chaque match de la règle donnée. Le premier argument de la fonction est `struct ipt_match_entry' et les autres arguments (s'il y en a) sont ceux fournis à la macro IPT_MATCH_ITERATE(). La fonction doit soit retourner 0 pour que l'itération continue, soir une valeur non nulle pour arrêter l'itération.
Cette fonction prend un pointeur vers une `entry' la taille totale de la table des `entry's, et une fonction à appeler à chaque fois. Le premier argument de la fonction doit être la `struct ipt_entry', et les autres arguments (s'il y en a) sont ceux fournis à la macro IPT_ENTRY_ITERATE(). La fonction doit retourner zéro pour que l'itération continue, ou une valeur non nulle pour arrêter l'itération.
Le userspace a 4 opérations: il peut lire la table courante, lire les infos (les positions des hooks et taille des tables), remplacer la table (et prendre les anciens compteurs), et ajouter de nouveaux compteurs.
Cela permet de simuler n'importe quel opération atomique à partir du userspace : cela est fait par la librairie `libiptc' qui fournit les fonctions utiles "add/delete/replace" pour les programmes.
Parce que ces tables sont transférées dans le kernelspace, l'alignement peut devenir un problème quand les règles d'alignement du userspace et du kernel sont différentes (par exemple: sparc64 avec un userspace 32 bits). Ces cas sont gérés en écrivant par dessus la définition de `IPT_ALIGN' pour ces plate-formes dans libiptc.h
Le kernel commence à traverser à partir de la location indiquée par le hook. Cette règle est examinée, si l'élément `struct ipt_ip' match, chaque `struct ipt_entry_match' est vérifie à son tour (la fonction associée avec chaque match est appelée). Si la fonction match retourne 0, l'itération stoppe sur cette règle. Si la fonction met le paramètre `hotdrop' à 1, le paquet va être aussi immédiatement détruit (c'est utilisé pour quelque paquets louches, comme dans la fonction tcp match).
Si l'itération continue jusqu'à la fin, les compteurs sont incrementés, la `struct ipt_entry_target' est examinée : si c'est une target standard, le champs `verdict' est lu (une valeur négative signifie un verdict du paquet, sinon, une valeur positive signifie un offset de déplacement). Si la réponse est positive et que l'offset n'est pas celui de la règle suivante, le drapeau `back' est mis, et la dernière valeur de `back' est reportée dans le champs `comefrom' de la règle.
Pour les targets pas standards, la fonction target est appelée : elle retourne un verdict (les targets pas standards ne peuvent sauter ("jump"), comme cela casserait le code qui détecte les paquets qui bouclent sans fin). Le verdict peut être IPT_CONTINUE pour continuer à la règle suivante.
Parce que je suis fénéant, iptables
est assez
extensible. C'est typiquement une combine pour refiler le boulot à
d'autres gens, ce pour quoi est fait l'Open Source de toutes façons
(cf. Free Software, qui, comme dirait Richard Stalleman, est à propos
de la liberté, et j'ai assisté à un de ses discours quand j'ai écris
ça).
Étendre iptables
comprend généralement 2 parties : Étendre
le kernel, en écrivant un nouveau module, et aussi optionnellement
étendre le programme iptables
en userspace, en écrivant une
nouvelle librairie partagée.
Écrire un module pour le kernel en soit même est relativement simple, comme vous pouvez le voir dans les exemples. Une chose qu'il faut savoir cependant, c'est que votre code doit être re-entrant : il peut y avoir un paquet qui arrive du userspace pendant qu'un autre arrive à cause d'une interruption. En fait, avec SMP, il peut même y avoir un paquet venant d'une interruption par CPU dans les 2.3.4 et au dessus.
Les fonctions à connaître sont:
C'est le point d'entrée du module. Il retourne un nombre négatif représentant le code d'erreur ou 0 si il s'est enregistré avec succès avec netfilter.
C'est le point de sortie du module. Il devrait se dé-enregistrer de netfilter.
C'est utilisé pour enregistrer un nouveau type de match. Vous lui donnez une `struct ipt_match', qui est habituellement déclarée comme une variable `static' (pour le fichier entier).
C'est utilise pour enregistrer un nouveau type de target. Vous lui donnez une `struct ipt_target', qui est habituellement déclarée comme une variable `static' (pour le fichier entier).
Utilisé pour dé-enregistrer votre target.
Utilisé pour dé-enregistrer votre match.
Un avertissement avant de fournir des trucs louches (comme fournir des compteurs) dans la place restante de votre nouveau match ou target. Sur les machines SMP, la table entière est dupliquée en utilisant `memcpy()' pour chaque CPU : si vous voulez vraiment garder l'information centrale, vous devez aller voir la méthode qu'utilise le match `limit'.
Les nouvelles fonctions de match son généralement écrites comme des modules à part entière. Il est ainsi possible d'avoir ces modules extensible à leur tour, mais ça n'est pas nécessaire habituellement. Un autre moyen serait d'utiliser la fonction `nf_register_sockopt' du canevas netfilter pour permettre à l'utilisateur de parler directement à votre module. Un autre moyen finalement est d'exporter les symboles pour que d'autre modules puissent s'enregistrer eux même, de la même manière que netfilter et ip_tables le font.
Le centre de votre nouvelle fonction de match est la structure `ipt_match' qui est fournie à `ipt_register_match()'. Cette structure à les champs suivants :
Ce champs est mis à n'importe quoi, par exemple disons `{NULL, NULL}'.
Ce champs est le nom de la fonction de match, comme appelée dans le userspace. Le nom doit être le même que le nom de fichier du module (par exemple si le nom est "mac", le nom de du module doit être "ipt_mac.o") pour que le chargement automatique des modules fonctionne.
Ce champs est un pointeur sur la fonction de match, qui prend le skb, les pointeurs sur les interfaces d'entrée "in" et de sortie "out" (un desquels peut être NULL, selon le hook), un pointeur vers les données de match de la règle en cours (la structure qui à été préparée en userspace), l'offset IP (une valeur non nulle signifie qu'il s'agit d'un fragment (pas le premier)), un pointeur sur le header du protocole, la longueur des données (la taille du paquet moins la taille du header IP) et finalement, un pointeur vers une variable `hotdrop'. La fonction doit retourner une valeur non nulle si le paquet match, et peut mettre le `hotdrop' à 1 si elle retourne 0, pour indiquer que le paquet doit être détruit immédiatement.
Ce champs est un pointeur vers une fonction qui vérifie les spécifications d'une règle. Si la fonction retourne 0, alors la règle ne sera pas acceptée. Par exemple, le match "tcp" ne va accepter seulement que des paquets TCP, et donc, si la `struct ipt_ip' qui fait partie de la règle ne spécifié pas explicitement que les paquets doivent être de type tcp, 0 est retourné. L'argument `tablename' vous permet de controler dans quelle table votre match peut être utilisé, et le 'hook_mask' est un masque de bit de hooks qui définit de quel hooks cette règle peut être appelé : si votre match n'a aucun sens dans certain hooks de netfilter, vous pouvez éviter ça ici.
Ce champs est un pointeur vers une fonction qui est appelée quand une entrée utilisant ce match a été effacée. Cela vous permet d'allouer dynamiquement des ressources dans 'checkentry' et de les nettoyer ici.
Ce champs est mis à 'THIS_MODULE', ce qui donne un pointeur vers votre module. Cela cause l'incrémentation ou la décrémentation du compteur d'utilisation, chaque fois qu'une règle utilisant votre module est ajoutée ou effacée. Cela permet d'éviter à un utilisateur d'enlever explicitement le module (et par conséquent d'appeler la fonction cleanup_module()) si une règle utilise encore votre module.
Si votre target modifie un paquet (par exemple le header ou le corps du paquet), elle doit appeler la fonction skb_unshare() pour copier le paquet au cas où le paquet est cloné. Sinon n'importe quelle raw socket qui a un clone du skbuff verra les modifications (les gens verront des trucs bizarre arriver avec tcpdump).
Les nouvelles targets sont habituellement écrites comme des modules à part entière. La discussion à ce propos trouvée dans la section ci-dessus `Nouvelles Fonctions de match' s'applique ici aussi.
Le centre de votre nouvelle target est la `struct ipt_target' qui est passée en paramètre à la fonction ipt_register_target(). Cette structure a les champs suivants:
Ce champs est mis à n'importe quelle valeur, disons `{NULL, NULL}'.
Ce champs est le nom de la fonction de target, comme appelée dans le userspace. Le nom doit correspondre à celui du module (par exemple, si le nom est "REJECT", le module doit s'appeler "ipt_REJECT.o") pour que le chargement automatique des modules fonctionne.
Ceci est un pointeur vers la fonction de target, qui prend en paramètre le skbuff, le numéro de hook, 2 pointeurs sur les périphériques d'entrée et de sortie (l'un ou l'autre pouvant être NULL), un pointeur sur des données de target, et la position de la règle dans la table. Cette fonction de target peut soit retourner IPT_CONTINUE (-1) si la traversé doit continuer, ou un verdict netfilter (NF_DROP, NF_ACCEPT, NF_STOLEN, etc...).
Ce champs est un pointeur vers une fonction qui vérifie les spécifications d'une règle. Si elle retourne 0, alors la règle ne sera pas acceptée.
Ce champs est un pointeur vers une fonction qui est appelée quand une entrée utilisant cette target est effacée. Cela vous permet d'allouer dynamiquement des ressources dans 'checkentry' et de les libérer ici.
Ce champs est mis à `THIS_MODULE', ce qui donne un pointeur vers votre module. Ceci cause l'incrémentation ou la décrémentation du compteur d'utilisation quand une règle utilisant votre target est ajoutée ou effacée. ça évite qu'un utilisateur puisse enlever votre module si une règle s'en sert encore.
Vous pouvez créer une nouvelle table pour vos propres besoins si vous le souhaitez. Pour faire cela, vous devez appeler la fonction `ipt_register_table()', avec une `struct ipt_table' qui a les champs suivants :
Ce champs est mis à n'importe quelle valeur, disons `{NULL, NULL}'.
Ce champs est le nom de la fonction de table, comme appelée dans le userspace. Le nom doit correspondre à celui du module (par exemple, si le nom est "nat", le module doit s'appeler "iptable_nat.o") pour que le chargement automatique fonctionne.
Ceci est une `struct ipt_replace' déjà remplie, comme utilisée dans le userspace pour remplacer une table. Le pointeur `counters' doit être mis à NULL. Cette structure de données peut être déclarée comme `__initdata' pour être oubliée après le boot.
Ceci est un masque de bits pour définir les hooks IPv4 de netfilter que la table va utiliser. Ceci est utilisé pour vérifier que les points d'entrée sont valides, et pour calculer les hooks possibles pour les fonctions checkentry() utilisées dans ipt_match et ipt_target.
Ceci est un `spinlock' en lecture-écriture pour la table entière. Initilisez le à RW_LOCK_UNLOCKED.
Ceci est utilisé en interne par le code d'ip_tables.
Vous avez écrit votre superbe module kernel dont vous voulez
contrôler les options à partir du userspace. Plutôt que d'avoir une
branche différente d'iptables
pour chaque extension,
j'utilise la dernière technologie des années 90: les librairies
partagées.
Les nouvelles tables n'ont pas besoin d'extension à
iptables
généralement : l'utilisateur utilise simplement
l'option `-t' pour utiliser la nouvelle table.
Les librairies partagées devraient avoir une fonction `_init()', qui sera appellée automatiquement après leur chargement : c'est un peu équivalent à la fonction `init_module()' qu'on utilise dans les modules kernel. Cette fonction devrait appeler `register_match()' ou `register_target()' respectivement si votre librairie partagée fournit un nouveau match ou une nouvelle target.
Vous devez fournir une librairie partagée : elle peut être utilisée pour initialiser une partie de la structure, ou fournir des options additionnelles. J'insiste maintenant sur une librairie partagée, même si celle-ci ne fait rien, pour limiter les problèmes.
Il y a plusieurs fonctions utiles décrites dans le header `iptables.h', spécialement :
Vérifie qu'un argument est en fait un `!', et si c'est le cas, met le flag `invert' s'il n'est pas déjà mis. Si ça renvoie vrai, vous devriez incrémenter `optind', comme fait dans les exemples.
convertit une chaîne de caractères en un nombre, dans l'interval demandé, retournant -1 si la chaîne est malformée ou si le nombre sort de l'interval.
Doit être appelée si une erreur a été trouvée. Habituellement, le premier argument est `PARAMETER_PROBLEM', ce qui signifie que l'utilisateur n'a pas utilisé la ligne de commande correctement.
La fonction `_init()' de votre librairie partagée donne à `register_match()' un pointeur sur une `struct iptables_match' static, qui a les champs suivants:
Ce pointeur est utilisé pour faire une liste chaînée de matchs (comme pour lister les règles). Il doit être mis à `NULL' au début.
Le nom de la fonction de match. Ceci doit correspondre au nom de la librairie (par exemple "tcp" pour "libipt_tcp.so").
Habituellement, on met IPTABLES_VERSION (macro) :
c'est fait pour être sûr que le programme iptables
ne charge
pas la mauvaise version d'une librairie partagée par erreur.
La taille des données de match pour ce match. Vous devriez utilisez la macro IPT_ALIGN() pour vous assurer que c'est correctement aligné.
Pour quelques matches, le kernel change quelques champs en interne (la target `limit' en est un bon exemple). Cela signifie que un simple `memcmp()' n'est pas suffisant pour comparer deux règles (nécessaire pour la fonction effacer-la-règle-correspondante). Si c'est le cas, placez tous les champs qui ne changent pas au début de la structure, et mettez la taille des champs inchangés ici. Bien sur, la plupart du temps cette valeur sera égale à celle du champs `size'.
Une fonction qui imprime le message d'aide pour l'option.
Ceci peut être utilisé pour initialiser de l'espace supplémentaire (si besoin est) dans la structure ipt_entry_match, et initialiser les bits de `nfcache'. Si vous examinez quelque chose qui n'est pas exprimable à l'aide du contenu de `linux/include/netfilter_ipv4.h', alors affectez simplement ce champs en faisant un OR avec NFC_UNKNOWN. Cette fonction sera appelée avant `parse()'.
Cette fonction est appelée quand une option non reconnue est vue sur la ligne de commande : elle devrait retourner une valeur non nulle si effectivement cette option vous été destinée. `invert' est vrai si un `!' a été déjà vu. Le pointeur `flags' est pour l'utilisation exclusive de votre librairie de match, et il est généralement utilisé pour garder un masque de bits contenant les options déjà spécifiées. Faites bien attention à ajuster les bits de `nfcache'. Vous pouvez étendre la taille de la structure `ipt_entry_match' en ré-allouant de l'espace si nécessaire, mais alors il faut vous assurer que la taille est passée en utilisant la macro IPT_ALIGN().
Cette fonction est appellée après que la ligne de commande a été examinée, est reçois en paramètre la variable entière `flags' réservée à votre librairie. Cela vous donne une dernière chance de vérifier que n'importe quel option obligatoire a bien été spécifiée, par exemple appelez la fonction `exit_error()' s'il en manque une.
Cette fonction est utilisée par le code de listage de règles pour imprimer sur la sortie standard les informations spécifiques à votre match (s'il y en a) pour une règle. le flag `numeric' est mis à vrai si l'utilisateur a spécifié l'option `-n' sur la ligne de commande.
Cette fonction est l'inverse de `parse' : elle est utilisée par `iptables-save' pour reproduire les options utilisées pour créer la règle.
C'est une liste `NULL-terminated' (terminée par
un NULL) d'options supplémentaires que votre librairie offre. Cette
liste est mélangée aux autres options d'iptables
qui sont
gérées par `getopt_long()'. Allez voir la man-page de `getopt_long'
pour plus de détails. L'argument retourné par getopt_long devient le
premier argument (`c') pour votre fonction `parse()'.
Il y a des éléments additionnels à la fin de cette structure réservés
pour usage interne à iptables
, vous n'avez pas besoin de les
utiliser.
La fonction `_init()' de votre librairie partagée donne à `register_target()' un pointeur vers une `struct iptables_target' static, qui a des champs similaires aux champs de la `struct iptables_match' décrite au-dessus.
libiptc
est la librairie de contrôle de iptables, étudiée
pour lister et manipuler les règles dans le module kernel
iptables. Bien qu'elle est actuellement principalement utilisée par le
programme iptables, cela rend beaucoup plus facile l'écriture de
nouveaux utilitaires. Vous devez être `root' pour utiliser ces fonctions.
Les tables kernel elles même sont une table de règles, et un jeu de nombre représentant les points d'entrée. Les noms de chaînes ("INPUT", etc...) sont fournies en tant qu'abstraction par la librairie. Les chaînes crées par l'utilisateur sont étiquetées en insérant un noeud d'erreur avant la tête de la chaîne définie par l'utilisateur, qui contient le nom de la chaîne dans la section de données supplémentaires de la target (la position des chaînes par défaut sont définies par les trois points d'entrée de la table).
Les targets standard suivantes sont supportées : ACCEPT, DROP, QUEUE (qui sont traduites respectivement en NF_ACCEPT, NF_DROP, ND_QUEUE), RETURN (qui est traduit en spécialement en IPT_RETURN qui sera géré par iptables) et JUMP (traduit à partir du nom de la chaîne courante vers un offset dans la table).
Quand `iptc_init()' est appelé, la table, incluant les compteurs sont lus. Cette table est manipulée par les fonctions `iptc_insert_entry()', `ipt_replace_entry', `iptc_replace_entry()', `iptc_append_entry()', `iptc_delete_entry()', `iptc_delete_num_entry()', `iptc_flush_entries()', `iptc_zéro_entries()', `iptc_create_chain()' `iptc_delete_chain()', et `iptc_set_policy()'.
Les changements portés sur la table ne sont pas ne sont pas effectués jusqu'à ce que vous appeliez la fonction `iptc_commit()'. Ce qui signifie que deux utilisateurs manipulant la même chaîne en même temps vont rencontrer des problèmes. `locking' (pour la synchronisation) est nécessaire pour éviter cela, mais ça n'est pas encore implémenté.
Cependant, il n'y a pas de condition de course avec les compteurs. Les compteurs sont re-ajoutés au kernel de telle sorte que l'incrémentation d'un compteur entre la lecture et l'écriture de la table se voit toujours.
Il y a quelques fonctions utiles :
Cette fonction retourne le nom de la première chaîne de la table.
Cette fonction retourne le nom de la chaîne suivante dans la table, NULL signifie qu'il n'y a plus de chaînes.
Retourne vrai si le nom de la chaîne suivante correspond à une chaîne qui est pré-définie.
Ceci retourne un pointeur vers la première règle de la chaîne dont on a donné le nom, NULL pour une chaîne vide.
Cette fonction retourne une pointeur vers la prochaine règle dans la chaîne dont a donné le nom, NULL pour la fin de la chaîne.
Cette fonction retourne la target d'une règle donnée. Si c'est une target étendue, le nom de la target est retourné. Si c'est un saut vers une autre chaîne, le nom de la chaîne est retourné. Si c'est un verdict (par exemple DROP), ce nom est retourné. Si la règle n'a pas de target (règle de comptage par exemple) alors une chaîne nulle est retournée.
Notez que cette fonction devrait être utilisée à la place d'utiliser directement la valeur qui est dans le champs `verdict' de la structure `ipt_entry', comme elle contient les interprétations que l'on a expliqué plus haut.
Cette fonction retourne la politique d'une chaîne pré-définie, et rempli l'argument `counters' avec les statistiques d'utilisation de cette politique.
Cette fonction retourne une explication plus sensée d'un code d'erreur qui s'est passe dans le code de la librairie iptc. Si une fonction plante, elle mettra à jour la valeur de `errno' : cette valeur peut être passée à `ipt_sterror()' pour retourner le message d'erreur correspondant.
Bienvenue dans le monde du `Network Address Traslation (NAT)' (Traduction D'adresses Réseau). Notez que l'infrastructure offerte a été étudiée pour plus de complétion que de vitesse, et que des modifications mineures futures pourront certainement augmenter l'efficacité du code. Pour le moment, je suis déjà content que ça marche !
NAT est séparée en `connection tracking' (suivi des connexions) qui ne manipule pas les paquets du tout, et le code de NAT lui même. Le suivi de connexions est aussi étudié pour être utilisé par un module spécial de iptables, et donc fait des différences subtiles d'état de connexion que NAT n'a pas besoin.
Le suivi de connexions s'accroche dans le début du hook NF_IP_LOCAL_OUT et NF_IP_PRE_ROUTING, de telle sorte qu'il puisse voir les paquets avant qu'ils n'entrent dans le système.
Le champs `ncft' dans le skb est un pointeur vers un des tableaux infos[] dans la structure ip_contrack. Du coup, on est capable de dire l'état du skb en sachant vers quel élément de ce tableau il pointe : ce pointeur encode la structure d'état et la relation entre ce skb vis-à-vis de cet état.
Le meilleur moyen d'extraire le champs `ncft' est d'appeler la fonction `ip_conntrack_get()', qui retourne NULL si `ncft' n'est pas mis, et rempli le `ctinfo' qui décrit la relation entre le paquet et la connexion. Cette énumération peut avoir plusieurs valeurs :
Le paquet fait partie d'une connexion établie, dans la direction originale.
Le paquet est en relation avec la connexion, et il est en train de passer dans la direction originale.
Le paquet est en train d'essayer de créer une nouvelle connexion (évidement, il est dans la direction originale).
Le paquet fait partie d'une connexion établie, mais dans la direction réponse.
Le paquet est en relation avec une connexion, mais il est dans la direction réponse.
Donc, un paquet réponse peut être identifie en testant la condition `>= IP_CT_IS_REPLY'.
Ces canevas sont étudiés pour accommoder n'importe quel type de protocole et type de mapping. Quelques un de ces types de mapping peuvent être très spécifiques, comme le type de mapping pour le load-balancing et le fail-over.
En interne, le code de suivi de connexions convertit un paquet en un n-uplet qui représente les parties intéressantes du paquet, avant de chercher les règles qui correspondent. Ce n-uplet a une partie manipulable, et une partie non manipulable, appelées respectivement "src" et "dst", comme il s'agit du point de vue du code du Source NAT quand il voit le premier paquet (ce serait un paquet réponse dans le code du Destination NAT). Le n-uplet pour chaque paquet d'une même connexion dans le même sens est tout simplement le même.
Par exemple, le n-uplet d'un paquet TCP contient la partie manipulable : adresse IP source et port source, et la partie non manipulable : l'adresse IP de destination et le port de destination. Les parties manipulable et non manipulable ne sont pas nécessairement du même type. Par exemple, un n-uplet d'un paquet ICMP contient une partie manipulable : adresse IP source et l'id icmp, et la partie non manipulable : l'adresse IP de destination et l'id icmp et code icmp.
Chaque n-uplet a un inverse, qui est le n-uplet du paquet réponse dans le flux. Par exemple, l'inverse d'un paquet icmp ping avec un numéro id 12345 en provenance de 192.168.1.1 vers 1.2.3.4 est un paquet icmp icmp pong avec un numéro id 12345 en provenance de 1.2.3.4 vers 192.168.1.1.
Ces n-uplets, représentés par la `struct ip_contrack_tuple', sont utilisé largement. En fait, avec le hook d'où provient le paquet (qui a effet sur le type de manipulation attendu) et l'interface en question, cela est en fait l'information complète d'un paquet.
La plupart des n-uplets sont contenus dans un `struct ip_conntrack_tuple_hash', qui ajoute une entrée a une liste doublement chaînée, et un pointeur vers la connexion à laquelle appartient ce n-uplet.
Une connexion est représentée par une `struct ip_conntrack' : ça a 2 champs `struct ip_conntrack_tuple_hash' : un qui référence la direction du paquet originel (tuplehash[IP_CT_DIR_ORIGINAL]), et 1 qui référence un paquet dans la direction réponse (tuplehash[IP_CT_DIR_REPLY]).
De toutes façons, la première chose que le code de NAT fait est de voir si le code de suivi de connexions a réussi à extraire un n-uplet et à trouver une connexion existante, en regardant le champs `nfct' du skbuff. Cela nous dit si c'est une tentative de nouvelle connexion, ou sinon, dans quelle direction le paquet va, auquel cas les manipulations déterminées précédemment sont faite.
Si c'était le début d'une nouvelle connexion, on cherche une règle qui correspond à ce n-uplet, en utilisant le mécanisme standard d'iptables de traversé, sur la table `nat'. Si une règle match, c'est utilisé pour initialiser les manipulations pour les 2 directions. Le code de suivi de connexions est notifié que la réponse qu'il doit attendre est maintenant différente. Alors, c'est manipulé comme au dessus.
Si il n'y a pas de règles, une attache `null' est crée : elle ne correspond pas au paquet, mais existe pour être sur qu'on n'accroche pas un autre flux existant. De temps en temps l'attache nulle ne peut pas être crée parce que l'on a déjà attaché un flux existant dessus, dans quel cas la manipulation par-protocole peut essayer de le ré-attacher, même si c'est en fait une attache nulle.
Les targets standard de NAT sont comme les autres extensions de target d'iptables, mis à part qu'elle insistent sur le fait de n'être utilisées que dans la table `nat'. Aussi bien la target SNAT que DNAT prennent une `struct ip_nat_multi_range' en données externes. C'est utilisé pour spécifier l'interval d'adresses qu'une attache est autorisée à utiliser. Un interval consiste en une `struct ip_nat_range' et en une adresse IP maximum inclusive, une adresse IP minimum inclusive, un minimum et maximum inclusifs pour les valeurs spécifiques au protocole (par exemple des ports pour TCP). Il y a aussi de la place pour des flags qui disent si une adresse IP peut être masquée (de temps en temps on ne veut masquer que la partie spécifique au protocole et pas IP), et une autre pour dire si l'interval concernant la partie spécifique au protocole est valide.
Un `multi-range' est un tableau de ces éléments `struct_ip_nat_range'. Ça veut dire qu'un interval peut être "1.1.1.1-1.1.1.2 ports 50-55 ET 1.1.1.3 port 80". Chaque élément interval ajoute sont interval (une `union' pour ceux qui aiment les théories mathématiques).
Implémenter un nouveau protocole signifie d'abord décider qu'est ce que les parties manipulable et non manipulable doivent être. Un n-uplet a la propriété d'identifier un flux de manière unique. La partie manipulable du n-uplet est celle avec laquelle vous pouvez faire la DNAT : pour TCP c'est le port source, pour ICMP c'est l'id icmp. Quelque chose à utiliser comme identifiant de flux. La partie non manipulable est le reste du paquet qui identifie le paquet de manière unique, mais on ne peut pas jouer avec (ex: port de destination d'un paquet TCP, le type d'un paquet ICMP).
Une fois que vous vous êtes décidé, vous pouvez écrire une extension au code de suivi de connexions dans le répertoire, et commencer à remplir la structure `ip_conntrack_protocol' que vous devez passer en paramètre à `ip_conntrack_register_protocol()'.
Les champs d'une `struct ip_conntrack_protocol' sont :
Mis à `{NULL, NULL}', utilisé pour vous coudre dans la liste.
Votre numéro de protocole, voir `/etc/protocols'.
Le nom de votre protocole. C'est le nom que l'utilisateur va voir. C'est mieux si c'est le non canonique trouvé dans `/etc/protocols'.
La fonction qui remplis les parties du n-uplet spécifiques au protocole, donné le paquet. Le pointeur `datah' pointe vers le début de votre header (juste après le header IP), et la variable `datalen' contient la longueur du paquet. Si le paquet n'est pas assez long pour contenir les informations du header, retourne 0. Néanmoins, `datalen' sera toujours au moins 8 octets (forcé par le canevas).
Cette fonction est simplement utilisée pour changer la partie spécifique au protocole du n-uplet en ce à quoi ressemblerai le n-uplet d'une réponse au paquet.
Cette fonction est utilisée pour imprimer la partie spécifique au protocole du n-uplet. Généralement, il s'agit de `sprintf()'s dans le tampon fournis. Le nombre de caractères du tampon est retourné. C'est utilisé pour imprimer les états de connexions dans une entrée /proc.
Cette fonction est utilisée pour imprimer la partie privée de la structure conntrack, si il y en une, aussi utilisé pour imprimer l'état de la connexion dans /proc.
Cette fonction est appelée quand un paquet qui fait partie d'une connexion établie est vu. Vous recevez un pointeur vers la structure conntrack, vers le header IP, la longueur, et le `ctinfo'. Vous retournez un verdict pour le paquet (habituellement NF_ACCEPT), ou -1 ne fait pas partie d'une connexion valide. Vous pouvez effacer la connexion à l'intérieur de cette fonction, mais vous devez suivre la marche suivante (pour éviter les conditions de course) :
if (del_timer(&ct->timeout))
ct->timeout.function((unsigned long)ct);
Cette fonction est appelée quand un paquet crée une nouvelle connexion pour la première fois. Il n'y pas de paramètre `ctinfo' puisque le premier paquet a toujours une `ctinfo' égale a IP_CT_NEW par définition. Ça retourne 0 pour rater la création de connexion, ou un timeout de la connexion en jiffies.
Une fois que vous avez écrit et testé que vous pouvez suivre le nouveau protocole, il est temps d'apprendre à NAT comment traduire l'adresse. ça veut dire écrire un nouveau module, une extension au code de NAT. Vous devez remplir la structure `ip_nat_protocol' que vous devez passer en paramètre à la fonction `ip_nat_protocol_register()'.
Mettez le à '{ NULL, NULL }'; Utilisé pour vous coudre dans la liste.
Le nom de votre protocole. Ce nom est le nom que l'utilisateur verra. C'est mieux si c'est le nom canonique du protocole comme trouvé dans `/etc/protocols', pour le chargement automatique des modules par le userspace, comme on le verra plus tard.
Votre numéro de protocole, voir `/etc/protocols'
C'est la deuxième moitié de la fonction pkt_to_tuple du code de suivi de connexions : vous pouvez la comparer à un "tuple_to_pkt". Il y a néanmoins quelques différences : vous recevez un pointeur sur le début du header IP, et la longueur totale du paquet. C'est parce que quelques protocoles (UDP, TCP) ont besoin de connaître le header IP. Vous recevez le champs `ip_nat_tuple_manip' du n-uplet (ex: le champs "src"), plutôt que le n-uplet entier, et le type de manipulation que vous avez à effectuer.
Cette fonction est utilisée pour dire si la partie manipulable du n-uplet est dans l'interval donné. Cette fonction est un peu difficile : on vous donne le type de manipulation qui a été effectué sur le n-uplet ce qui nous dit comment interpréter l'interval (est ce que c'est un interval source ou un interval destination dont on parle ?)
Cette fonction est utilisée pour vérifier si une attache existante nous met dans l'interval voulu, et aussi pour vérifier si aucune manipulation n'est nécessaire du tout.
Cette fonction est le noyau de NAT : donné un n-uplet et un interval, on va changer la partie spécifique au protocole du n-uplet pour le mettre dans l'interval et le rendre unique. Si on ne peut pas trouver de n-uplet pas encore utilisé dans l'interval, retourne 0. On reçoit aussi un pointeur vers la structure conntrack, qui est requise par `ipt_nat_used_tuple()'.
L'approche habituelle est de simplement itérer la partie spécifique au protocole du n-uplet dans l'interval, en vérifiant avec `ip_nat_used_tuple()' dessus, jusqu'à ce que ça retourne faux.
Notez que le cas d'attache nulle a déjà été vérifie : soit c'est en dehors de l'interval donné, soit c'est déjà pris.
Si IP_NAT_RANGE_PROTO_SPECIFIED n'est pas encore mis, ça signifie que l'utilisateur fait du NAT, et pas du NAPT : soyez sensibles avec les intervals. Si aucune attache n'est désirable (par exemple, dans TCP, une attache de destination ne devrait pas changer le port TCP sauf si on le lui demande), retournez 0.
Donné un tampon de caractères, match un n-uplet et un masque, et écrit la partie spécifique au protocole, et finalement retourne la longueur du tampon utilise.
Donné un tampon de caractères et un interval, écrit l'interval faisant partie de la partie spécifique au protocole du n-uplet, et retourne la longueur du tampon utilisé. Ce ne sera pas appelé si le flag IP_NAT_RANGE_PROTO_SPECIFIED n'a pas été mis à vrai pour l'interval.
C'est vraiment la partie intéressante. Vous pouvez écrire de nouvelles target NAT qui fournissent de nouvelles type d'attaches : deux targets supplémentaires sont fournies dans le package par défaut : MASQUERADE et REDIRECT. Elles sont assez simple pour illustrer le potentiel et la puissance d'écrire de nouvelles targets NAT.
Elles sont écrites comme n'importe quelles autres target iptables, mais en interne, elles vont extraire la connexion et appeler la fonction `ip_nat_setup_info()'.
Les Protocol helpers permettent au code de suivi de connexions de comprendre certains protocoles qui utilisent de multiples connexions réseaux (ex: FTP) et de marquer la connexion `fille' comme étant en relation ('RELATED') à la connexion initiale, habituellement en lisant les adresses en relation dans le flux de données.
Les Protocol helpers pour Nat font 2 choses : d'abord elles permettent au code de NAT de manipuler le flux de données pour changer l'adresse contenu dedans, et ensuite de s'occuper du NAT sur la connexion en relation quand elle arrive, basé sur la connexion originale.
Le devoir du module de suivi de connexions est de spécifier quels paquets appartient à quel connexions déjà existantes. Le module a les moyens suivant pour faire ça :
S'il y a plus de travail à effectuer au premier paquet reçu d'une connexion en relation, le module peut enregistrer une fonction callback qui sera rappelée à ce moment.
La fonction `init' de votre module kernel doit appeler `ip_conntrack_helper_register()' avec un pointeur vers une `struct ip_conntrack_helper'. Cette structure a les champs suivants :
C'est le header pour la liste chaînée. Netfilter gere cette liste en interne. Initilisez la juste à `{ NULL, NULL }'.
Un pointeur vers une chaîne de caractères contenant le nom du protocole. ("ftp", "irc", "...")
Un groupe d'option avec un ou plusieurs des flags suivants :
Un pointeur sur la structure de module. Initialisez ça à la macro `THIS_MODULE'.
Nombre maximum de connexions attendues qui ne sont pas encore confirmées (en attente).
Le temps de vie maximum (en secondes) pour chaque connexion attendue. Une connexion attendue est effacée `timeout' secondes après que l'attente de connexion a été émise par la fonction `ip_conntrack_expect_related()'.
C'est une `struct ip_conntrack_tuple' qui spécifié les paquets par lesquels notre Helper pour le suivi de connexions est intéressé.
Encore une fois, une `struct ip_conntrack_tuple'. Ce
masque spécifie quels bits du tuple
sont valides.
La fonction que Netfilter doit appeler pour chaque paquet qui match le n-uplet+mask.
#define FOO_PORT 111
static int foo_expectfn(struct ip_conntrack *new)
{
/* appelé quand le premier paquet d'une connexion attendue arrive */
return 0;
}
static int foo_help(const struct iphdr *iph, size_t len,
struct ip_conntrack *ct,
enum ip_conntrack_info ctinfo)
{
/* Analyse les données passées sur cette connexion et
décide à quoi les paquets en relation vont ressembler */
à
/* Mets à jour les données privées de chaque connexion maîtresse (session, état, ...) */
ct->help.ct_foo_info = ...
if (il_y_aura_de_nouveaux_paquets_en_relation_avec_cette_connexion)
{
struct ip_conntrack_expect exp;
memset(&exp, 0, sizeof(exp));
exp.t = tuple_specifying_related_packets;
exp.mask = mask_for_above_tuple;
exp.expectfn = foo_expectfn;
exp.seq = tcp_sequence_number_of_expectation_cause;
/* les données des connexions esclaves.
exp.help.exp_foo_info = ...
ip_conntrack_expect_related(ct, &exp);
}
return NF_ACCEPT;
}
static struct ip_conntrack_helper foo;
static int __init init(void)
{
memset(&foo, 0, sizeof(struct ip_conntrack_helper);
foo.name = "foo";
foo.flags = IP_CT_HELPER_F_REUSE_EXPECT;
foo.me = THIS_MODULE;
foo.max_expected = 1; /* Une attente à la fois */
foo.timeout = 0; /* L'attente n'expire jamais */
/* Nous sommes intéressés par tous les paquets TCP avec un
port destination de 111 */
foo.tuple.dst.protonum = IPPROTO_TCP;
foo.tuple.dst.u.tcp.port = htons(FOO_PORT);
foo.mask.dst.protonum = 0xFFFF;
foo.mask.dst.u.tcp.port = 0xFFFF;
foo.help = foo_help;
return ip_conntrack_helper_register(&foo);
}
static void __exit fini(void)
{
ip_conntrack_helper_unregister(&foo);
}
Les Helper Modules pour NAT gèrent la partie spécifique à une application du NAT. Habituellement, ça inclue la manipulation de données à la volée : pensez à la commande 'PORT' en FTP, quand le client dit au serveur sur quel port se connecter. Donc le Helper Module pour NAT de FTP doit remplacer l'adresse IP et le port après la commande PORT dans connexion de contrôle FTP.
Si on fait du TCP, les choses deviennent un peu plus compliquées. La raison est que la taille du paquet peut changer (ex: FTP, la longueur de la chaîne de caractères représentant une adresse IP/Port n-uplet après la commande PORT a changé). Si on change la taille du paquet, on a un SYN/ACK de différence entre le coté gauche et le coté droit de la machine NAT. (ex: si on a étendu un paquet de 4 octets, on doit ajouter cet offset au numéro de séquence pour chaque paquets suivants)
NAT pour le paquet en relation doit être géré spécialement aussi. Prenez encore comme exemple FTP, quand tous les paquets qui arrivent de la connexion DATA doivent être NATés à l'adresse IP/port donnés par le client avec la commande PORT sur la connexion de contrôle, plutôt que de passer dans les tables normalement.
la fonction `init()' de votre Helper Module pour NAT appelle `ip_nat_helper_register()' avec un pointeur vers une `struct ip_nat_helper'. Cette structure a les champs suivants:
Mettez la à `{ NULL, NULL }'
Un pointeur sur une chaîne de caractères contenant le nom du protocole.
Un groupe d'option avec un ou plusieurs des flags suivants :
>Un pointeur sur la structure de module. Initialisez ça à la macro `THIS_MODULE'.
Une structure `struct ip_conntrack_tuple' décrivant par quels paquets notre Helper Module pour NAT est intéressé.
Une `struct ip_conntrack_tuple' disant à Netfilter
quels bits tuple
sont valides.
La fonction d'aide qui est appelée pour chaque paquet qui match n-uplet+masque.
La fonction d'attente appelée pour chaque premier paquet de la connexion.
C'est a peu près la même chose que d'écrire un helper de connexion.
#define FOO_PORT 111
static int foo_nat_expected(struct sk_buff **pksb,
unsigned int hooknum,
struct ip_conntrack *ct,
struct ip_nat_info *info)
/* Appellée des que l'on reçoit le premier paquet d'une connexion en relation.
params: pksb le tampon de paquets
hooknum Le hook duquel l'appel vient (POST_ROUTING, PRE_ROUTING).
ct les informations à propos de cette connexion (en relation).
info &ct->nat.info
return value: Le verdict (NF_ACCEPT, ...)
{
/* Change le port/adresse IP du paquet vers leur valeurs masqueradées
(lu à partir de master->tuplehash), pour le faire de la même manière,
appelez ip_nat_setup_info, retournez NF_ACCEPT. */
}
static int foo_help(struct ip_conntrack *ct,
struct ip_conntrack_expect *exp,
struct ip_nat_info *info,
enum ip_conntrack_info ctinfo,
unsigned int hooknum,
struct sk_buff **pksb)
/* Appellée pour chaque paquet où conntrack a détecté une attente
params: ct La struct ip_conntrack de la connexion maîtresse.
exp La struct ip_conntrack_expect de l'attente causée par
le helper conntrack pour ce protocole.
info État (STATE) : related, new, established, ... )
hooknum Le hook duquel l'appel vient (POST_ROUTING, PRE_ROUTING).
pksb Le tampon de paquets.
*/
{
/* Extrait les informations à propos des futurs paquets en relation
(vous pouvez partager l'information avec la fonction foo_help
du suivi de connexions). Échange ip adresse/port avec leur
valeur masqueradée, insère le n-uplet des paquets en relation */
}
static struct ip_nat_helper hlpr;
static int __init(void)
{
int ret;
memset(&hlpr, 0, sizeof(struct ip_nat_helper));
hlpr.list = { NULL, NULL };
hlpr.tuple.dst.protonum = IPPROTO_TCP;
hlpr.tuple.dst.u.tcp.port = htons(FOO_PORT);
hlpr.mask.dst.protonum = 0xFFFF;
hlpr.mask.dst.u.tcp.port = 0xFFFF;
hlpr.help = foo_help;
hlpr.expect = foo_nat_expect;
ret = ip_nat_helper_register(hlpr);
return ret;
}
static void __exit(void)
{
ip_nat_helper_unregister(&hlpr);
}
Netfilter est relativement simple, et il est décrit de manière assez complète dans les sections précédentes. Cependant, il est nécessaire de temps en temps d'aller en dessous de ce que les infrastructure NAT ou ip_tables offrent, ou vous pourriez vouloir les remplacer complètement.
Une chose importante pour Netfilter (oui enfin...dans le future ) est le `caching'. Chaque snk a un champs `nfcache' : un masque de bits décrivant quel champs du header on été examinés, et si oui ou non le paquet a été modifié. L'idée est que chaque hook de netfilter fait un OR (OU binaire) avec les bits qui le concernent, pour que plus tard on puisse écrire un système de cache qui serait suffisamment intelligent pour réaliser quand un paquet n'a pas besoin du tout de passer à travers netfilter.
Les bits les plus importants sont NFC_ALTERED, signifiant que la paquet a été modifié (c'est déjà utilisé par le hook NF_IP_LOCAL_OUT d'IPv4 pour re-router les paquets modifiés) et NFC_UNKNOWN, qui signifie que le caching ne doit pas être fait, parce que quelques propriétés qui ne peuvent pas être exprimées ont été examinées. Si vous ne savez pas, mettez simplement le flag NFC_UNKNOWN sur le champs `nfcache' du skb à l'intérieur de votre hook.
Pour recevoir/modifier des paquets à l'intérieur du kernel, vous pouvez simplement écrire un module qui enregistre un "hook Netfilter". C'est intéressant pour certaines choses. Les points effectifs sont spécifiques à chaque protocole, et définis dans les headers de protocole de netfilter, comme "netfilter_ipv4.h" par exemple.
Pour enregistrer et dé-enregistrer un hook netfilter, vous devez utiliser les fonctions `nf_register_hook()' et `nf_unregister_hook()'. Elles prennent chacune un pointeur sur une `struct nf_hook_ops' qui a les champs suivants :
Initialisez la à '{ NULL, NULL }'.
La fonction à appeler quand un paquet touche ce point de hook. Votre fonction doit retourner NF_ACCEPT, NF_DROP ou NF_QUEUE. Si elle retourne NF_ACCEPT, le hook suivant attaché à ce point va être appelé. Si elle retourne NF_DROP, le paquet est détruit. Si elle retourne NF_QUEUE, le paquet est queuté. Vous recevez un pointeur vers un pointeur de skb, donc vous pouvez remplacer totalement le skb si vous le souhaitez.
Pour le moment inutilisé : fait dans l'idée de passer le hit de paquets quand le cache est vide. Peut ne jamais être implémenté : mettez le à NULL.
La famille de protocole, par exemple `PF_INET' pour IPv4.
Le numéro du hook qui vous intéresse, par exemple `NF_IP_LOCAL_OUT'.
Cette interface est pour le moment utilisée par ip_queue. Vous pouvez vous enregistrer pour gérer les paquets queutés pour un protocole donné. ça ressemble à l'enregistrement d'un hook, sauf que vous pouvez faire que le paquet soit bloqué pour un certain temps, et aussi que vous ne voyiez que les paquets pour lesquels un hook a répondu `NF_QUEUE'.
Les 2 fonctions utilisées pour s'enregistrer en tant que partie intéressée par les paquets queutés sont : `nf_register_queue_handler()' et `nf_unregister_queue_handler()'. Les fonctions que vous enregistrez seront appelées avec le pointeur `void *' que vous avez donné à `nf_register_queue_handler()'.
Si personne n'est enregistré pour gérer un protocole, alors un verdict NF_QUEUE devient équivalent à un verdict NF_DROP.
Une fois que vous vous êtes enregistré pour recevoir les paquets queutés, ils commencent à arriver. Vous pouvez faire ce que vous voulez avec ces paquets, mais vous devez appeler `nf_reinject()' quand vous en avez fini avec eux (ne faites pas simplement un kfree_skb() sur eux). Quand vous ré-injectez un skb, vous donnez en paramètre à la fonction : le skb, la structure `struct nf_info' que le gestionnaire vous a donné, et un verdict : NF_DROP provoquera la destruction du paquet, NF_ACCEPT fera que l'itération à travers les hooks reprendra, NF_QUEUE fera qu'il seront queutés encore une fois, et NF_REPEAT fera que le hook qui a queuté le paquet soit reconsulté (attention aux boucles sans fin !!).
Vous pouvez regarder dans le `struct nf_info' pour obtenir des informations supplémentaires à propos du paquet, comme les interfaces et hooks qu'il a fréquenté.
Il est commun pour les composants de Netfilter de vouloir interagir avec le userspace. La méthode pour faire cela est d'utiliser le mécanisme `setsockopt()'. Notez que chaque protocole doit être modifié pour appeler nf_setsockopt() avec un nombre setsockopt qu'il ne comprend pas (et nf_getsockopt() avec un nombre getsockopt qu'il ne comprend pas), et pour le moment seulement IPv4, IPv6, et DECnet on été modifiés.
En utilisant maintenant une technique familière, on enregistre une `struct nf_sockopt_ops' en utilisant la fonction `nf_register_sockopt()'. Les champs de cette structure sont :
Mettez le à '{ NULL, NULL }'.
La famille de protocole que vous gérez, ex: PF_INET.
et
Ceux la spécifient l'interval (exclusif) de nombre setsockopt gérés. Et donc, 0 et 0 signifie que vous n'avez pas de nombre setsockopt.
C'est la fonction qui est appelée quand un utilisateur appelle une de vos setsockopts. Vous devriez vérifier dans cette fonction qu'il a la capabilité `NET_ADMIN'.
and
Ceux-là spécifient l'interval (exclusif) de nombres getsockopt gérés. Et donc, 0 et 0 signifie que vous n'avez pas de nombre getsockopt.
C'est la fonction qui est appelée quand un utilisateur appelle une de vos getsockopts. Vous devriez vérifier dans cette fonction qu'il a la capabilité `NET_ADMIN'.
Les deux champs finaux sont utilisés en interne.
En utilisant la librairie `libipq' et le module `ip_queue', presque tout ce qui peut être fait au niveau du kernel peut maintenant être fait en userspace. Ça veut dire qu'avec une petite perte de vitesse, vous pouvez développer votre code entièrement en userspace. A moins que vous vouliez filtrer une grosse bande passante, vous devriez trouver cette approche supérieure à la modification de paquets dans le kernel.
Dans les débuts de Netfilter, j'ai prouvé ça en portant une version embryonique de iptables en userspace. Netfilter vous ouvre la porte pour que plus de gens puissent écrire des modificateurs de paquets efficaces, dans n'importe quel langage.