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.
Structures de données d'ip_tables
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 :
Une `struct ipt_entry'.
0 ou plus `struct ipt_entry_match', chacune avec une taille
variable (0 ou plus octets) de données accrochées à elles.
Une `struct ipt_entry_target', avec une taille variable de
données (0 ou plus octets) accrochées à elles.
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:
Une `struct ipt_ip', qui contient les spécifications pour le
header IP qui est à matcher.
Un tableau de bits `nf_cache' qui montre quel partie du paquet
cette règle a examiné.
Un champs `target_offset' indiquant l'offset à partir du début
de la règle, où la structure ipt_entry_target commence. Ça devrait
être toujours aligné correctement à l'aide de la macro
IPT_ALLIGN().
Un champs `next_offset' indiquant la taille totale la règle, en
incluant les matches, et la target. Ça doit être aussi toujours
aligné correctement, à l'aide de la macro IPT_ALLIGN().
Un champs `comefrom' utilisé par le kernel pour suivre la trace
des paquets dans leur traversé.
Un champs `struct ipt_counters' contenant le paquet et les
compteurs d'octets pour les paquets qui on matché cette règle.
`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
:
ipt_get_target()
Cette fonction `inline' retourne un pointeur vers la target de
la règle.
IPT_MATCH_ITERATE()
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.
IPT_ENTRY_ITERATE()
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.
ip_tables à partir du userspace
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
Utilisation d'ip_tables et la Traversé.
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.
Le Kernel
É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:
init_module()
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.
cleanup_module()
C'est le point de sortie du module. Il devrait se dé-enregistrer
de netfilter.
ipt_register_match()
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).
ipt_register_target()
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).
ipt_unregister_target()
Utilisé pour dé-enregistrer votre target.
ipt_unregister_match()
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.
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 :
list
Ce champs est mis à n'importe quoi, par exemple disons `{NULL,
NULL}'.
name
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.
match
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.
checkentry
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.
destroy
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.
me
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.
Nouvelles Targets
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:
list
Ce champs est mis à n'importe quelle valeur, disons `{NULL,
NULL}'.
name
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.
target
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...).
checkentry
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.
destroy
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.
me
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.
Nouvelles Tables
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 :
list
Ce champs est mis à n'importe quelle valeur, disons `{NULL,
NULL}'.
name
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.
table
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.
valid_hooks
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.
lock
Ceci est un `spinlock' en lecture-écriture pour la table
entière. Initilisez le à RW_LOCK_UNLOCKED.
private
Ceci est utilisé en interne par le code d'ip_tables.
Outils userspace
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 :
check_inverse()
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.
string_to_number()
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.
exit_error()
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.
Nouvelles fonctions de match
La fonction `_init()' de votre librairie partagée donne à
`register_match()' un pointeur sur une `struct iptables_match'
static, qui a les champs suivants:
next
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.
name
Le nom de la fonction de match. Ceci doit correspondre au nom de
la librairie (par exemple "tcp" pour "libipt_tcp.so").
version
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.
size
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é.
userspacesize
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'.
help
Une fonction qui imprime le message d'aide pour l'option.
init
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()'.
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().
final_check
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.
print
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.
save
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.
extra_opts
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.
Nouvelles fonctions de target.
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.
Utiliser `libiptc'
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 :
iptc_first_chain()
Cette fonction retourne le nom de la première chaîne de la
table.
iptc_next_chain()
Cette fonction retourne le nom de la chaîne suivante dans la
table, NULL signifie qu'il n'y a plus de chaînes.
iptc_builtin()
Retourne vrai si le nom de la chaîne suivante correspond à une
chaîne qui est pré-définie.
iptc_first_rule()
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.
iptc_next_rule()
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.
iptc_get_target()
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.
iptc_get_policy()
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.
iptc_strerror()
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.
Connection Tracking (Suivi de connexions)
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 :
IP_CT_ESTABLISHED
Le paquet fait partie d'une connexion établie, dans la direction
originale.
IP_CT_RELATED
Le paquet est en relation avec la connexion, et il est en train
de passer dans la direction originale.
IP_CT_NEW
Le paquet est en train d'essayer de créer une nouvelle connexion
(évidement, il est dans la direction originale).
IP_CT_ESTABLISHED + IP_CT_IS_REPLY
Le paquet fait partie d'une connexion établie, mais dans la
direction réponse.
IP_CT_RELATED + IP_CT_IS_REPLY
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
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).
Nouveaux protocoles
Dans le kernel
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 :
list
Mis à `{NULL, NULL}', utilisé pour vous coudre dans la
liste.
proto
Votre numéro de protocole, voir `/etc/protocols'.
name
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'.
pkt_to_tuple
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).
invert_tuple
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.
print_tuple
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.
print_conntrack
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.
paquet
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);
new
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()'.
list
Mettez le à '{ NULL, NULL }'; Utilisé pour vous coudre dans la
liste.
name
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.
protonum
Votre numéro de protocole, voir `/etc/protocols'
manip_pkt
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.
in_range
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.
unique_tuple
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.
print
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.
print_range
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.
Nouvelles targets NAT
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()'.
Protocol Helpers
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.
Helpers Modules pour le suivi de connexions
Description
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 :
Dire à Netfilter par quel paquet notre module est intéressé (la
plupart de Helpers opèrent sur un port en particulier).
Enregistrer une fonction avec Netfilter. Cette fonction est
appelée pour chaque paquet qui match le critère du dessus.
Une fonction `ip_conntrack_expect_related()' qui peut être
appelée pour dire à Netfilter de s'attendre à des connexions en
relation.
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.
Structures et Fonctions Disponibles
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 :
list
C'est le header pour la liste chaînée. Netfilter gere cette
liste en interne. Initilisez la juste à `{ NULL, NULL }'.
name
Un pointeur vers une chaîne de caractères contenant le nom du
protocole. ("ftp", "irc", "...")
flags
Un groupe d'option avec un ou plusieurs des flags suivants :
IP_CT_HELPER_F_REUSE_EXPECT Re-utilise les entrées de
connexions attendues si la limite (voir max_expected ci-dessous)
est atteinte.
me
Un pointeur sur la structure de module. Initialisez ça à la
macro `THIS_MODULE'.
max_expected
Nombre maximum de connexions attendues qui ne sont pas encore
confirmées (en attente).
timeout
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()'.
tuple
C'est une `struct ip_conntrack_tuple' qui spécifié les paquets
par lesquels notre Helper pour le suivi de connexions est
intéressé.
mask
Encore une fois, une `struct ip_conntrack_tuple'. Ce masque
spécifie quels bits du tuple sont valides.
help
La fonction que Netfilter doit appeler pour chaque paquet qui
match le n-uplet+mask.
Exemple de base d'un Helper Module pour le suivi deconnexions
:
#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);
}
Helper Modules pour NAT
Description
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.
un `callback' pour le paquet qui a généré la connexion en
relation (foo_help)
un `callback' pour tous les paquets en relation
(foo_nat_expected)
Structures et Fonctions disponibles
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:
list
Mettez la à `{ NULL, NULL }'
name
Un pointeur sur une chaîne de caractères contenant le nom du
protocole.
flags
Un groupe d'option avec un ou plusieurs des flags suivants :
IP_NAT_HELPER_F_ALWAYS Appelle le NAT helper pour chaque
paquet, et pas seulement pour les paquets où le module de suivi de
connexion a détecté une attente.
IP_NAT_HELPER_F_STANDALONE Dit au coeur de NAT que ce NAT
helper n'a pas de module de suivi de connexions, mais qu'il a
seulement un NAT helper.
me
>Un pointeur sur la structure de module. Initialisez ça à la
macro `THIS_MODULE'.
tuple
Une structure `struct ip_conntrack_tuple' décrivant par quels
paquets notre Helper Module pour NAT est intéressé.
mask
Une `struct ip_conntrack_tuple' disant à Netfilter quels bits
tuple sont valides.
help
La fonction d'aide qui est appelée pour chaque paquet qui match
n-uplet+masque.
expect
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.
Exemple de module de NAT Helper
#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 :
list
Initialisez la à '{ NULL, NULL }'.
hook
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.
flush
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.
pf
La famille de protocole, par exemple `PF_INET' pour IPv4.
hooknum
Le numéro du hook qui vous intéresse, par exemple
`NF_IP_LOCAL_OUT'.
S'occuper des paquets Queutés
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é.
Recevoir des commandes à partir du userspace.
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 :
list
Mettez le à '{ NULL, NULL }'.
pf
La famille de protocole que vous gérez, ex: PF_INET.
set_optmin
et
set_optmax
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.
set
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'.
get_optmin
and
get_optmax
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.
get
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'.
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.