PF pour les nuls

GNU/Linux Magazine HS n° 029 | mars 2007 | Landry (gaston) Breuil
Creative Commons
  • Actuellement 0 sur 5 étoiles
0
Merci d'avoir participé !
Vous avez déjà noté cette page, vous ne pouvez la noter qu'une fois !
Votre note a été changée, merci de votre participation !
Maman, j'ai peur d'internet, y'a que des pirates pédophiles nazis et des blogueurs du web 3.2alpha, ou « comment jouer au videur de boîte de nuit avec les paquets réseau ?». Miam, ça c'est de l'intro !

1. Kessessé ?

1.1 Une petite page d'histoire

PF est né d'une histoire classique dans le monde du Logiciel libre, un problème de licence. A l'origine, OpenBSD utilisait depuis la création du monde (événement connu sous le nom de la-grande-décision-de-théo) un système de firewalling/filtrage nommé « IP Filter » (aka IPF), écrit par Darren Reed [1]. Cependant, un changement [2] [3] [4] dans la licence d'IPF, ainsi que plusieurs désaccords sur son architecture, aboutissent à l'éviction [5] de celui-ci d'OpenBSD peu après la sortie de la version 2.9 de l'OS. Cet événement donna par ailleurs aussi lieu au grand-audit-des-licences-terreuses.

OpenBSD se retrouva alors sans système de filtrage de paquets. C'est à ce moment qu'un semi-dieu, nommé Daniel-le-bienveillant (aussi connu sous le surnom de « l'homme aux longs cheveux qui venait du pays des horloges », ou plus communément dhartmei@) entreprit [6] [7] [8], à partir d'un burin, un marteau et un bloc de granit, la création de ce qui allait devenir PacketFilter, un puissant système de filtrage de paquets avec suivi d'états (stateful packet filter) à la syntaxe claire comme de l'eau de roche. PF fut intégré avec la bénédiction de Théo-le-magnifique dans OpenBSD dès la version 3.0, et depuis a été porté dans la douleur [9] sur NetBSD 2.0, et a aussi remplacé [10] IPF dans FreeBSD à partir de la version 5.3 et dans DragonFlyBSD à partir de la version 1.2. PF roulaize over da world et oune IPF. Oups, désolé, je m'emporte, refermons la page historique ici.

1.2 Alors donc, ça fait quoi cet engin ?

PF est donc un filtre de paquets réseau fonctionnant en espace noyau, c'est-à-dire qu'il se place au niveau de la pile réseau et inspecte tous les paquets entrants et sortants de la machine, et en fonction de règles de filtrage va prendre la décision de :

- bloquer silencieusement le paquet (dans l'espace, personne n'entend les air-lutins crier) ;

- rejeter le paquet en envoyant un TCP RST à l'expéditeur (j'ai dit : « PAS DE BASKETS @#! ») ;

- laisser passer le paquet, en le marquant éventuellement au fer rouge pour post-traitement (Alors ça j'y mets là, ça j'y colle ici..).

PF utilise en plus de ce filtrage une table d'états des connexions établies, permettant de laisser passer les paquets d'une connexion existante sans qu'aucun traitement ne leur soit appliqué. Cette table permet aussi de faire des vérifications sur les numéros de séquence TCP, de vérifier qu'un ICMP echo reply correspond à un ICMP echo request...

Enfin, PF permet aussi (oui, madame Michu, ce n'est pas tout) de faire de la NAT (-ation *dzim-boum* calembour (c)Mat`). Vous en aviez rêvé, vous allez pouvoir partager votre connexion internet avec tout votre réseau local en faisant de la traduction d'adresses (oui, je sais, ça vient de mon réseau local en adresse privée, mais il faut que ça aille fissa vers le grand nain Ternet et que ça revienne aussi vite à son maî-maître), faire de la redirection de ports, séduire votre voisine (hep, chérie, t'as vu tous les poils que j'ai mis dans cette règle de filtrage ? HaoOOon whoaaaa, prends-moi toute !!) et bien d'autres choses encore...

Pour faire tout cela, PF utilise un simple fichier texte pour sa configuration, par défaut /etc/pf.conf. Ce fichier contient une liste de règles. Nous reviendrons sur sa syntaxe après un intermède trollifère.

1.3 Et par rapport à Iptables, ça va leverager mon outsourcing ?

Alors évidemment, je l'attendais la grande question. Oui, évidemment, PF surpasse Iptables, PF enfonce Iptables dans une taupinière et lui met des coups de bêche. Je ne sais même pas pourquoi je devrais me justifier. Objectivement :

- Sa syntaxe de configuration est beaucoup plus concise et logique : par exemple, pour rediriger ce qui arrive sur le port 80 extérieur vers le port 8080 d'une machine sur le réseau local, la règle PF serait :

rdr on $ext proto tcp from any to $ext port 80 -> \

192.168.40.1 port 8080

L'équivalent Iptables :

iptables -t nat -A PREROUTING -p tcp -i eth0 --dport 80 \

    -j DNAT --to 192.168.0.2:8080

iptables -A FORWARD -p tcp -i eth0 -d 192.168.40.1 \

--dport 8080 -j ACCEPT

- Il s'interfaçe nativement avec des outils de haute disponibilité (CARP/Pfsync) et de routage (OpenBGPD/OpenOSPFD).

- Il permet nativement de faire de la gestion de bande passante (QoS) avec AltQ.

- Il propose un proxy FTP, pendant en plus mieux bien de ip_conntrack_ftp chez NetFilter/Iptables.

- On peut faire de la création de règles à la connexion d'un utilisateur via authpf, utile pour forcer les utilisateurs à s'authentifier avant d'autoriser le trafic à passer. (Hop, je me connecte à distance sur mon firewall. Tant que je suis connecté, mon trafic perso passe. Hop, je me déconnecte, ça revient à la normale.)

- On peut faire de la reconnaissance d'OS (je redirige les Solaris vers tel serveur, les Windows XP vers Dave Null..).

- Tous les changements de règles au niveau du moteur de filtrage sont des opérations atomiques.

- dhartmei@ il a plein de poils, alors que les codeurs Iptables, ils en ont une toute petite.

- et j'en passe. En gros, ce sont deux projets ayant le même but, mais qui se basent sur des conceptions et idées différentes. Chacun voit midi à sa porte, YMMV, toussa. T'façon, j'y connais keudalle en Iptables.

La seule différence notable au niveau de la configuration par rapport à Iptables se trouve dans l'ordre d'interprétation des règles : par défaut dans PF, la dernière règle qui correspond au paquet emporte la décision (« last matching rule wins »), alors que pour Iptables l'analyse des règles s'arrête à la première correspondant au paquet. Cependant, nous verrons que le mot clé quick permet de passer outre ce comportement.

Par la suite, mes exemples prendront comme hypothèse que PF est sur une machine fonctionnant comme une passerelle entre internet (interface réseau externe) et un réseau local (interface réseau interne), mais, bien évidemment, on peut utiliser PF sur une machine personnelle avec une seule interface réseau, et aussi sur un serveur avec 3/4/2142 interfaces, comme par exemple une zouliiie Soekris. Par flemme, j'utiliserai aussi l'anglicisme « matcher une règle », lorsqu'un paquet correspond aux critères d'une règle. Oui, traitez-moi de feignant, j'aime ça.

2. Comment que ça marche le bouzin ?

2.1 Quoi, un bête fichier texte pour le configurer ?

La configuration de PF se fait donc via /etc/pf.conf, dont la syntaxe est simple : les lignes vides sont ignorées, les lignes commençant par un # sont des commentaires et $variable est remplacé par la valeur de variable. Ce fichier est principalement (je ne rentre volontairement pas dans les détails) composé de 5 sections.

2.1.1 Les macros et listes

On peut définir des macros pour remplacer des variables revenant souvent, ou par souci de clarification : ports, noms d'interfaces, adresses IP...

if_ext="rl0"

webserver="192.168.1.10"

Les listes permettent, quant à elles, de rassembler des critères dans une variable :

block out on fxp0 proto tcp from \

    { 192.168.0.1, 10.5.32.6 } to any port { 22 80 }

Cette liste sera « déroulée » lors du chargement de la règle correspondante, donnant ici lieu à quatre règles avec chacune des combinaisons des valeurs des listes. La virgule entre les valeurs d'une liste est facultative.

On peut, bien évidemment, mixer les macros et les listes, et écrire de jolies choses :

if_ext="rl0"

trusted = "{ 192.168.1.2 192.168.5.36 }"

#...

pass in on $if_ext inet proto tcp from \

    { 10.10.0.0/24 $trusted } to port 22

2.1.2 Les tables

Elles permettent de stocker un grand nombre d'adresses (50 ou 50000, même combat), et sont ensuite utilisées directement dans les règles de filtrage/NAT/redirection. La recherche d'une adresse dans une table en mémoire est beaucoup plus rapide, et moins coûteuse en temps processeur/mémoire que la recherche dans un jeu de règles correspondant chacune à une valeur d'une liste d'adresses.

Le mot-clé const est utilisé lorsqu'on veut que la table ne puisse pas être modifiée, et persist indique à PF de ne pas supprimer une table non référencée par une règle. L'avantage d'une table non const par rapport aux listes est que l'on peut ajouter/supprimer à la volée des adresses ou sous-réseaux dans une table, utile pour bloquer un certain temps l'adresse d'un spammeur ou d'un script-kiddie par exemple, ou gérer la redirection vers un ensemble de serveurs faisant de la haute disponibilité.

Enfin, cerise sur le gâteau, on peut initialiser une table avec un fichier contenant une liste d'adresses.

table <privateip> const \

    { 192.168.0.0/16, 172.16.0.0/12, 10.0.0.0/8 }

table <spammers> persist file "/etc/spammers"

#...

block in on fxp0 from { <privateip>, <spammers> } to any

...

root@spud[~]#pfctl -t spammers -T add 218.70.0.0/16

root@spud[~]#pfctl -t spammers -T delete 218.70.5.12

Les tables, c'est VIE, AMOUR et POIL QUI BRILLE (c) aflab.

2.1.3 Les règles de NAT

Basiquement, la NAT fait de la traduction d'adresses, c'est-à-dire qu'on modifie les paquets provenant d'un réseau en adressage privé pour qu'ils puissent transiter sur Internet. PF va remplacer l'adresse source (privée) dans un paquet sortant par l'adresse publique de la passerelle, et garder dans une table de correspondance la paire ip:port source + ip:port destination. Ainsi, quand on reçoit la réponse correspondant à ce paquet sortant, on remplace l'adresse destination (qui était notre adresse publique) par l'adresse privée de la machine ayant initié la connexion. Ce mécanisme marche bien entendu pour TCP, UDP ou ICMP, même si ces deux derniers sont des protocoles sans état. C'est bon, j'ai perdu personne ?

Tout d'abord, pour activer le forwarding de paquet, il faut mettre le sysctl net.inet.ip.forwarding à 1, via la commande sysctl ou le fichier /etc/sysctl.conf si on veut que le changement soit persistant. De plus, il ne faut pas oublier que les paquets vont passer à travers le filtre de paquets après avoir été modifiés, sauf si l'on utilise le mot-clé pass. Et ceci s'applique aussi à RDR que nous verrons juste après.

La syntaxe générique d'une règle de NAT est globalement la suivante :

nat [pass] on interface [address_family] from src_addr \

[port src_port] to dst_addr [port dst_port] -> ext_addr

Je décortique ces hiéroglyphes : ce qui est entre crochets est facultatif, ce qui est en rouge est variable.

- nat => c'est une règle de NAT.

- pass => le paquet est NATé, et envoyé directement sans passer par le filtre de paquets.

- on interface => le paquet est arrivé sur telle interface réseau (rl0, ne3, $ext_if, ppp0…)

- address_family => inet ou inet6, c'est un détail qui peut avoir son importance.

- from src_addr => le paquet provient de telle adresse. Ici, pour l'adresse, on peut spécifier plein de trucs :

 - une adresse simple en notation IPv4/IPv6 : 10.246.200.1

 - une plage d'adresses en notation CIDR : 162.168.10.0/24

 - un nom de domaine, il sera résolu par PF lors du chargement des règles.

 - une interface réseau : elle correspondra à toutes les adresses qui lui sont attribuées.

 - une table ou une liste, comme expliqué un peu plus haut.

 - n'importe laquelle de ces notations, précédée d'un ! pour signifier la négation.

 - et finalement le mot-clé magique any, pour n'importe quelle adresse.

- port src_port => si on veut NATer uniquement un certain port ou une plage de ports... rarement utilisé.

- to dst_addr => le paquet est destiné à telle adresse. Mêmes possibilités que pour src_addr.

- -> ext_addr => remplacer l'adresse source par cette adresse. Le retour se fera tout seul par magie. Et si cette adresse change (attribuée par DHCP), on peut ici spécifier le nom de l'interface réseau entre parenthèses (rl0), et l'adresse sera automatiquement mise à jour dans la règle. Ca, c'est *hooOOaaNnn*.

Donc, pour partager simplement votre connexion internet avec tout votre réseau local, il suffit d'une seule règle, par exemple (ne3 est ma carte réseau côté local, 10.246.200.0/24 est la notation CIDR de mon réseau local, et rl0 la carte réseau côté externe, branchée sur un modem en IP dynamique) :

nat on ne3 inet from 10.246.200.0/24 to any -> (rl0)

C'est-ty-pas beau ? Vous vous rappelez le bordel que c'est pour faire la même chose avec Iptables ?

2.1.4 Les règles de redirection de paquets

RDR est ici le petit frère caché de la NAT, à savoir qu'il fait tout l'inverse de son grand-frère : il prend les paquets arrivant de l'extérieur de la passerelle, et les redirige vers une machine du réseau local. Ça peut être très pratique quand on a un serveur web sur une machine de son réseau local, et qu'on veut qu'il soit directement accessible de l'extérieur, mais aussi dans des exemples plus pragmatiques comme « faire marcher le transfert de fichier avec Jabber », ou « rediriger un certain trafic venant de la Freebox vers mon Laptop pour pouvoir se ‘loutrifier’ devant la télé au pieu ». Oui, je sais. Honte, flemme, toussa.

Maintenant, le comment-kon-fait. La syntaxe générique d'une règle de RDR est globalement la suivante :

rdr [pass] on interface [address_family] from src_addr

[port src_port] to dst_addr [port dst_port]

    -> int_addr [port int_port]

C'est la même syntaxe que pour la NAT, à quelques petits détails près, du genre « je redirige ce qui était à destination de dst_addr:dst_port vers int_addr:int_port, et, comme pour la NAT, le retour se fera tout seul comme par magie.

Hop hop, un petit exemple, disons que je veux accéder de l'extérieur au service SSH d'un serveur de mon réseau local, mais en gardant l'accès SSH à ma passerelle. Je choisis un autre port extérieur (mettons 34522, oui j'aime me compliquer la vie à trouver des numéros de ports que je suis sûr d'oublier au moment où j'aurais besoin de m'en souvenir) et je redirige ce qui arrive sur ce port vers mon serveur ($external est le nom de mon interface réseau externe, et $diane est l'adresse privée du serveur sur le réseau local).

rdr pass on $external proto tcp to port 35422 -> $diane \

    port ssh

# et j'accède au serveur de l'extérieur par :

ssh -p 35422 my.gateway.info

Au passage, vous remarquerez que j'ai nommé directement le port ssh, et non 22. Oui, on peut utiliser tous les petits noms de /etc/services à chaque fois qu'on veut désigner un port.

Bon évidemment, mal foutu, tout ce genre de bricolage peut avoir des conséquences dramatiques sur la sécurité de votre réseau local, car la machine vers laquelle on redirige ce trafic (qui n'a pas forcément de firewall, ELLE) est directement accessible de l'extérieur sur ce port. Vous iriez vous balader tout nu dans la neige un 31 décembre avec 5 grammes d'alcool dans le sang ? Moi, non (Quoique, bref, *ahem*, passons !). Généralement, on met les machines auxquelles on peut accéder de l'extérieur de cette manière dans une DMZ, avec un politique de filtrage bien pensée. Bref, on fait attention à ce qu'on fait, rognnntudju !

D'ailleurs, houba houba hop, passons à la susdite politique de filtrage.

2.1.5 Les règles de filtrage

Comme dit précédemment, les règles de filtrage sont évaluées de façon séquentielle de la première à la dernière (du haut vers le bas dans les fichiers de règles utilisés). Ce qui veut dire que chaque paquet va être évalué par chaque règle, et la dernière règle correspondant au paquet emporte la décision (block ou pass). Au contraire, si on utilise le mot-clef quick, l'évaluation s'arrête dès qu'une règle correspond au paquet. La première règle (implicite) est un « tout laisser passer » de sorte que si aucune règle n'est applicable à un paquet, celui-ci est accepté, c'est pourquoi généralement la première règle explicite est un block all. On va pas laisser rentrer n'importe qui non plus.

Allez hop, comme pour les autres, on déshabille la syntaxe générale d'une règle :

action [direction] [log] [quick] [on interface] \

    [address_family] [proto protocol] \

    [from src_addr [port src_port]] \

    [to dst_addr [port dst_port]] \

    [flags tcp_flags] [state]

Bon, quoi de neuf là-dedans qu'on n'ait pas déjà vu... ?

- action => au choix, block ou pass. J'ai pas besoin de vous faire un dessin ? Ah si, un détail, la politique pour les paquets bloqués sera drop ou return en fonction de l'option block-policy. Par défaut, c'est drop.

- direction => in ou out, si on veut filtrer le trafic entrant ou sortant de l'interface. Si on ne met rien, la règle sera évaluée pour les deux sens.

- log => si ce flag est présent, on enregistre la décision prise pour cette règle concernant le paquet. Pour analyser ceci, pflog sera notre ami. Nous ferons sa connaissance un peu plus loin.

- quick => j'en ai déjà parlé, si ce flag est aussi présent et que le paquet matche la règle, il ne sera plus analysé/trituré, et la décision prise par cette règle sera notre dernier mot, Jean-Pierre.

- proto protocol => un protocole de niveau 4, généralement tcp, udp ou icmp, mais on peut aussi rencontrer n'importe quel protocole de niveau 4 référencé dans /etc/protocols. Si on veut faire un peu d'obfuscation, on peut l'appeler par son petit numéro même.

- port dst_port => petite précision que j'avais oublié tout à l'heure, emporté par mon élan fougueux, on peut ici spécifier un intervalle complexe de ports avec les opérateurs <, <=, >=, >, <>, <> et :, voyez avec l'ami man pf.conf pour plus de détails. Vive les hiéroglyphes, merci Spontex.

- flags tcp_flags_check/mask => on peut spécifier des vérifications supplémentaires sur les drapeaux d'un paquet TCP, par exemple pour traiter les ouvertures de session TCP. On utilise souvent flags S/SA que je traduirais par « cette règle s'applique sur les paquets TCP qui ont, sur les deux drapeaux SYN et ACK, seulement SYN positionné ». Si les 2 drapeaux sont positionnés, le paquet ne matchera pas la règle. Pour les autres flags, RTFM un peu.

- state => ici, on utilise généralement deux possibilités : keep state ou synproxy state. La première est utilisée quand on veut créer une entrée dans la table des états de connexions lorsqu'un paquet matche la règle, et appliquer la même politique aux paquets suivants prenant part à la connexion. Tous ces paquets sont donc rattachés à cette entrée, et on peut aussi du coup vérifier que la séquence des paquets TCP est bien respectée. synproxy state est utilisé quand on veut que PF joue le rôle d'un proxy TCP pour l'établissement d'une connexion. Dans ce cas, PF va traiter la demande en lieu et place du destinataire et ne transférera qu'ensuite les paquets à ce dernier. Aucun paquet n'est transmis au destinataire avant que le client n'ait terminé l'échange initial. Cette technique permet de protéger le destinataire des attaques de type TCP SYN flood, lorsqu'on demande un grand nombre d'ouvertures de connexions en vue de provoquer un déni de service.

Ça commence à devenir un peu indigeste tout ça, donc on va voir tout de suite deux exemples simples de règles :

pass in quick on $external inet proto tcp from any to any \

    port { http, https, smtp, imaps } flags S/SA keep state

En français, j'autorise tous les paquets TCP/IPv4 arrivant sur l'interface externe à destination des ports http/https/smtp/imaps à passer. Je vérifie que ce sont des ouvertures de connexion TCP, j'enregistre leur état dans la table, et j'arrête l'analyse de ces paquets sur cette règle (quick). Plutôt simple, non ?

block in log on $external from \

{ 192.168.0.0/16, 172.16.0.0/12, 10.0.0.0/8 } to any

Ici, je bloque les paquets arrivant sur l'interface externe avec une adresse source privée (et donc non routable), et j'enregistre les informations du paquet bloqué. Ça permet de parer certaines attaques de type spoofing, où on envoie des paquets usurpés dans le but d'induire en erreur les équipements réseau (pour plus d'informations sur cette technique, se référer à la RFC 2827 [11])

Bon évidemment, il y a encore pleins d'options, de détails et de particularités (comme les ancres, le scrubbing, l'antispoofing...) dont je n'ai pas parlé (et que je maîtrise pas, un peu d'humilité n'a jamais fait de mal). Pour plus d'informations, il faudra se reporter à la documentation officielle de pf.conf [12]. Attention, ça peut faire mal au crâne.

2.2 J'en fais quoi de ce pf.conf maintenant ?

L'interface principale côté utilisateur de PF est pfctl. Avant toute chose, il faut « activer » le device PF dans le noyau, soit manuellement via pfctl -e, soit en ajoutant pf=YES dans le fichier de configuration global du système lu lors du démarrage de la machine (généralement /etc/rc.conf.local). Si on veut faire de la NAT/RDR, ne pas oublier d'activer cette option dans le noyau via la commande sysctl net.inet.ip.forwarding=1, ou en ajoutant/décommentant la ligne net.inet.ip.forwarding=1 dans /etc/sysctl.conf pour rendre ce changement persistant.

Tout d'abord, on va se pencher sur les principales options de pfctl permettant de configurer PF :

- -d : désactive PF, l'opposé de -e. Une fois PF désactivé, plus rien n'est filtré/natté, les paquets transitent directement dans la pile réseau du noyau.

- -f fichier : le fichier de configuration à charger. (/etc/pf.conf par défaut)

- -n : ne pas réellement charger les règles, faire juste une analyse syntaxique du fichier.

- -o : activer les options d'optimisations. PF va réordonner les règles et supprimer les doublons pour optimiser le jeu de règles.

Ensuite, les options permettant de modifier à la volée l'état de PF :

- -T (kill/flush/add/delete/show/test/..) : utilisé conjointement avec -t table, permet de manipuler une table : la supprimer, la vider, ajouter une adresse, en supprimer une, l'afficher, vérifier qu'une adresse est dans la table... Exemple : pfctl -t blocked-hosts -T show va m'afficher les adresses de toutes les machines ayant été ajoutées dans la table blocked-hosts, déclarée un peu plus haut dans mon /etc/pf.conf.

- -F (nat/rules/state/Tables/..) : remet respectivement à zéro les règles de NAT, les règles de filtrage, les états des connexions ouvertes, ou les tables. Pratique, si on veut faire un peu de ménage, remettre à zéro des compteurs ou des connexions, désactiver la NAT, supprimer toutes les entrées de toutes les tables, etc..

- -k (host/network) : Permet de tuer toutes les entrées dans la table d'état concernant les connexions venant d'une machine/d'un réseau. Si on utilise deux fois cette option, on supprime les états des connexions venant de la première adresse vers la deuxième. Exemple : pfctl -k 192.168.1.0/24 -k 172.16.0.0/16 supprimera tous les états des connexions entre ces deux sous-réseaux.

Enfin, l'option permettant d'obtenir beaucoup d'informations sur le statut de PF, j'ai nommé -s modifieur, à utiliser conjointement avec -r si on veut que PF fasse des reverse-dns lookup pour les adresses qu'il affiche. Les valeurs les plus intéressantes pour modifieur sont rules, nat, state, et info, pour afficher respectivement les règles de filtrage chargées en mémoire, celles de NAT, les connexions ouvertes, et des statistiques globales sur PF (all permettra d'afficher tout ce que PF a à nous dire, tu vas les cracher ces infos 'spèce de balance @"!@#%*$!?). Hop hop, quelques exemples valent mieux qu'un long blabla :

root@spud[~]#pfctl -s nat

nat on ne3 inet from 10.206.210.0/24 to any

    -> 82.245.152.88

rdr pass on ne3 inet proto udp from 212.27.38.253 to any

    -> 10.206.210.174

rdr pass on ne3 inet proto tcp from any to any port = 1720

    -> 10.206.210.175

....

root@spud[~]#pfctl -s rules

block drop in quick from <blocked-hosts> to any

block drop all

pass quick on lo all

pass quick on rl0 all

pass in quick on ne3 inet proto tcp from any to any

    port = www flags S/SA keep state

pass in quick on ne3 inet proto tcp from any to any

    port = https flags S/SA keep state

pass in quick on ne3 inet proto tcp from any to any

    port = smtp flags S/SA keep state

pass in quick on ne3 inet proto tcp from any to any

    port = imaps flags S/SA keep state

pass out quick on ne3 inet proto tcp all flags S/SA

    keep state

pass out quick on ne3 inet proto udp all keep state

pass out quick on ne3 inet proto icmp all keep state

....

root@spud[~]#pfctl -r -s state

all tcp network.org:24680 -> anthony.freenode.net:6667

    ESTABLISHED:ESTABLISHED

all tcp renton.network.org:39832 -> network.org:62328

    -> jabber.freenet.de:5222 ESTABLISHED:ESTABLISHED

all tcp renton.network.org:33719 -> network.org:62794

    -> 205.188.7.181:5190   ESTABLISHED:ESTABLISHED

all tcp renton.network.org:55564 -> network.org:56919

-> zone6.gcu-squad.org:80   FIN_WAIT_2:FIN_WAIT_2

all tcp renton.network.org:55565 -> network.org:62487

    -> zone6.gcu-squad.org:80   FIN_WAIT_2:FIN_WAIT_2

all tcp network.org:80

    <- sek76-2-82-245-144-239.fbx.proxad.net:1143

FIN_WAIT_2:FIN_WAIT_2

all tcp network.org:80

    <- sek76-2-82-245-144-239.fbx.proxad.net:1161

    TIME_WAIT:TIME_WAIT

all tcp network.org:13150 -> pop.free.fr:110

    TIME_WAIT:TIME_WAIT

all tcp network.org:11892 -> firewall-services.com:995

    ESTABLISHED:ESTABLISHED

all udp network.org:26191 -> dns1.proxad.net:53

    MULTIPLE:MULTIPLE

all tcp network.org:33799 -> network.org:60594

    -> anelis.isima.fr:22   ESTABLISHED:ESTABLISHED

....

  

root@spud[~]#pfctl -s info

Status: Enabled for 99 days 08:19:47          Debug: Urgent

Interface Stats for ne3               IPv4             IPv6

  Bytes In                    110259968736                0

  Bytes Out                    99321680976              352

  Packets In

    Passed                       174547989                0

    Blocked                        1075543                0

  Packets Out

    Passed                       168492615                1

    Blocked                          26376                4

State Table                          Total             Rate

  current entries                        9

  searches                       691697504           80.6/s

  inserts                          8159941            1.0/s

  removals                         8159932            1.0/s

....

Pour toutes ces commandes, on peut utiliser -v pour rendre PF un peu plus bavard, voire -vv et même -g et -x pour le passer en mode debug, auquel cas il devient aussi saoulant qu'un vieil oncle ayant un peu trop pris d'apéros lors d'un repas de famille. L'option -q pourra alors permettre de lui faire fermer un peu sa gu...

Bien sûr, je n'ai pas la prétention de vous expliquer tout pfctl, la documentation de référence sera toujours la page de manuel officielle [13]. Voilà, avec ça je pense que vous êtes en mesure de passer aux exemples complets. Attention, interrogation écrite après cette section.

3. J'ai rien compris, dessine-moi un mouton

Tout d'abord, un premier exemple simple, voici notre cahier des charges (f34r d4 8uZzW0rD) :

- Nous avons une machine connectée à internet via une interface réseau en IP fixe, nommons-la bge0 pour l'exemple.

- Elle doit être accessible de l'extérieur en ssh à partir de trois machines dites « de confiance ».

- Son serveur Apache doit être accessible à tous.

- On veut qu'elle réponde aux pings de l'extérieur.

- On veut pouvoir accéder à internet à partir de cette machine, pratique pour lire de la doc (ou pompazer de la bonne stuffaize (*kh*)).

- Enfin, on bloquera silencieusement tous les autres paquets.

Hop, toutes ces règles se traduisent par ce /etc/pf.conf :

# Notre interface réseau

iface = "bge0"

# Les machines auxquelles on fait confiance,

# elles seront autorisées à se connecter via ssh.

trusted_hosts = "{ 131.25.4.12, 88.12.74.5, 207.124.20.9 }"

# On ne s'occupe pas de l'interface de loopback, utilisée

# par plusieurs services internes à la machine.

set skip on lo

# On active la normalisation de paquets en entrée. Pf

# va réassembler les paquets fragmentés et faire des

# vérifications supplémentaires dessus.

scrub in all

# Par défaut, on bloque tout les paquets.

block all

# On autorise les paquets icmp de type echo request pour

# les pings venant de l'extérieur, et echo reply/time

# exceeded/destination unreachable pour les réponses aux

# pings que l'on avait initié vers l'extérieur.

pass in inet proto icmp from any to $iface icmp-type \

    { echoreq, echorep, timex, unreach }

# On autorise les connexions au serveur Apache

# et on les enregistre dans la table d'état.

pass in inet proto tcp from any to $iface port www \

    flags S/SA keep state

# On autorise les connexions au service sshd, uniquement \

# en provenance des machines auxquelles on fait confiance.

pass in inet proto tcp from $trusted_hosts to $iface \

    port ssh flags S/SA keep state

# Enfin on autorise tout le trafic sortant.

pass out inet proto tcp from $iface to any flags S/SA \

    keep state

pass out inet proto { udp, icmp } from $iface to any \

keep state

Un coup de pfctl -e -f /etc/pf.conf et voilà, avec cette configuration notre machine est protégée ! Certaines règles sont assez strictes (généralement, on met all à la place de from any to $iface, on laisse passer tous les paquets ICMP, on utilise quick à outrance...), mais le filtrage que l'on désirait est en place. Faites une pause, et réfléchissez à ce qu'il aurait fallu faire pour obtenir la même chose avec Iptables. Un peu plus mieux bien, non ?

Mais je suppose que ça ne vous suffit pas (bande de petits coquins, il vous en faut toujours plus.). Passons donc à un exemple un peu plus évolué.

4. Et si j'ai envie d'en coller sur les murs et au plafond ?

Donc oui, maintenant on en vient à l'exemple que vous attendez tous. Comment faire une passerelle tout en un qui protège notre réseau local des attaques extérieures, permette de sortir couvert sur l'intairnaitte (si si, Roger et Lucette, ils l'appellent comme ça) plein de méchants, et tant qu'à faire fasse marcher des trucs convis qui utilisent des ports exotiques. Non, elle ne fera pas le café, sauf si vous achetez l'adaptateur kivabien.

On récapitule ce qu'on veut en bon françois des familles :

- Notre passerelle (renton) dispose de deux interfaces, rl0 côté internet et ne3 côté réseau local.

- La première possède une IP fixe (comme chez la plupart des fournisseurs d'accès internet, encore faut-il être dégroupé...)

- La seconde interface côté réseau local est configurée avec l'adresse 192.168.1.1 sur le réseau local en 192.168.1.0/24.

- On protégera tout ce beau monde de l'extérieur, comme on est parano, on fera un peu de logging.

- On veut que les machines de notre réseau local aient accès à internet sans limitation.

- Une machine particulière (diane) devra être accessible via ssh et HTTPS. (Oui, j'ai la flemme de la mettre dans une DMZ, blame on me.)

- On voudra accéder au multiposte de Free sur une machine (sickboy), c'est juste un flux RTSP/UDP classique.

- Notre machine principale (tommy) aimerait bien faire marcher des trucs clicka-compliants, comme le transfert de fichiers via Jabber.

- Enfin, on aimerait bien pouvoir accéder à notre passerelle de l'extérieur en ssh, des fois qu'une urgence se présente.

Vous êtes prêts ? Allez hop, on se jette dans un nouveau /etc/pf.conf !

# /etc/pf.conf, seconde édition

# - disclaimer - si cette configuration fait fondre

# votre passerelle, je décline toute responsabilité.

# Déclaration des interfaces réseau

ext="rl0"

int="ne3"

# Déclaration des ports à ne pas logguer

# (5900 est le port utilisé par VNC)

ports_not_logged = "{ netbios-ssn, microsoft-ds, \

    epmap, ms-sql-s, 5900}"

# Déclaration des hôtes sur mon réseau local

diane = "192.168.1.2"

tommy = "192.168.1.20"

sickboy = "192.168.1.21"

# Déclaration du port utilisé à l'extérieur pour accèder

# au sshd de diane

ssh_diane = "65322"

set skip on lo

scrub in all

# Tout d'abord, la règle principale de NAT pour le réseau

# local. Ici, je ne mets pas 'pass' car après j'autorise

# explicitement tout le trafic sortant. Le suffixe :network

# est utilisé pour dire 'le sous-réseau correspondant à

# l'adresse de cette interface'

nat on $ext from $int:network to any -> $ext

# Ensuite, les règles de redirection. J'ai mis le mot-clé

# 'pass' pour ne pas faire de filtrage supplémentaire sur

# ces connexions, sinon il aurait fallu rajouter les ports

# correspondants dans la règle de filtrage sur le trafic

# venant de l'extérieur.

# Le flux rtsp du multiposte free est de l'udp provenant

# de freeplayer.freebox.fr

rdr pass on $ext proto udp from 212.27.38.253 -> $sickboy

# On redirige le port 8010 utilisé par Jabber pour

# l'ouverture des transferts de fichiers entrants

rdr pass on $ext proto tcp to port 8010 -> $tommy

# Enfin on redirige le ssh (sur le port particulier utilisé

# à l'extérieur) et le https vers diane

rdr pass on $ext proto tcp to port $ssh_diane \

-> $diane port ssh

rdr pass on $ext proto tcp to port https -> $diane

# Petite nouveauté, on active l'antispoofing sur l'interface

# externe : cette règle bloquera les paquets venant de

# l'extérieur essayant d'utiliser frauduleusement notre

# adresse pour passer à travers le filtre.

antispoof for $ext

# On ne filtre pas les paquets sur l'interface interne.

pass quick on $int

# Maintenant, les règles de filtrage...

# Par défaut, on bloque et on loggue tout les paquets

# venant de l'extérieur.

block in on $ext log all

# On n'a pas envie de remplir nos logs en 5 minutes avec les

# quelques vers bien connus qui trainent sur internet

# donc on bloque certains paquets sans les logguer.

block in on $ext inet proto tcp from any to any \

port $ports_not_logged

# On autorise les pings en provenance de l'extérieur

pass in on $ext inet proto icmp from any to any icmp-type \

    { echorep, echoreq, timex, unreach }

# On autorise le ssh venant de l'extérieur

pass in on $ext inet proto tcp from any to any port ssh \

    flags S/SA keep state

# On autorise tout le trafic sortant (le trafic NATé du \

# réseau local passera par ces règles)

pass out on $ext inet proto tcp all flags S/SA keep state

pass out on $ext inet proto { udp, icmp } all keep state

Alors, heureux ? Ça vous paraît pas déjà plus clair avec quelques exemples ? Évidemment, il y aurait encore beaucoup à dire sur le sujet, mais je vous laisserai le plaisir de découvrir toutes les subtilités jouissives de la configuration d'un PF à 5h30 du matin, les yeux rougis par l'écran, les mains rendues tremblantes par la surdose de café (ou de bière, belge de préférence), l'aube dardant ses rayons d'argent par la fenêtre, alors que vous vous dites « Putain, c'est beau, une ville, la nuit. ». Rhalala, quelle poésie, on dirait du Pinpin.

Tiens, je m'apprêtais à prendre congé de vous à regret, mais je m'aperçois que nous avons utilisé un mot-clé dont je n'ai pas encore parlé en long et en large : log. Houba Hop, fissa, next section !

5. Et comment je fais joujou à la NSA ?

Donc, quand PF a envie de cafter un peu. Il va envoyer des informations sous un format binaire (histoire que ça soit plus rigolo, c'est du PCAP/TCPdump standard) sur une pseudo-interface réseau (pflog0), et un de ses bons potes, j'ai nommé pflogd, va stocker tout ça dans le fichier /var/log/pflog. Puis, nous aurons la joie de faire la connaissance d'un autre larron, nommé tcpdump, qui se chargera de traduire toutes ces informations en français académique. Si, si, je vous le jure, presque académique.

Tout d'abord, il faut activer/lancer le démon pflogd. Normalement, il doit se lancer tout seul si PF est activé au démarrage de la machine. Si ce n'est pas le cas : ifconfig pflog0 up && pflogd.

On vérifie que tout marche bien :

root@spud[~]#ps waux | grep pflog

root     29220 0.0 0.1   448   396 ?? Is    10Sep06

    0:00.02 pflogd: [priv] (pflogd)

_pflogd 13046 0.0 0.1   512   280 ?? S     10Sep06

    12:36.73 pflogd: [running] -s 116 -f /var/log/pflog

    (pflogd)

root@spud[~]#ifconfig pflog0

pflog0: flags=141<UP,RUNNING,PROMISC> mtu 33224

Maintenant que PF cause sur pflog0 lorsqu'un paquet matche une règle où le mot-clé log est utilisé, passons à tcpdump. On peut l'utiliser selon deux modes :

- interactif : tcpdump -i pflog0. Il ira directement lire ce qui passe en direct sur pflog0, pflogd sera donc inutile.

- passif : tcpdump -r /var/log/pflog. Il lira ce qui a été enregistré par pflogd dans son fichier de sortie.

Un exemple sera peut-être plus parlant :

root@spud[~]#tcpdump -qea -ttt -i pflog0

Dec 24 18:56:42.832166 rule 39/(match) block in on ne3:

    dslb-088-073-124-180.pools.arcor-ip.net.46554

    > my.network.org.2967: [|tcp] (DF)

Dec 24 18:56:46.077820 rule 39/(match) block in on ne3:

    dslb-088-073-124-180.pools.arcor-ip.net.46554

    > my.network.org.2967: [|tcp] (DF)

Dec 24 18:59:48.917072 rule 39/(match) block in on ne3:

    62.160.212.130.4376 > my.network.org.5900: [|tcp] (DF)

On peut aussi passer une expression à tcpdump pour qu'il filtre sa sortie selon des critères précis :

root@spud[~]#tcpdump -ttt -r /var/log/pflog port 80 and \

    host 192.168.1.2

Enfin, tcpdump comprend aussi la syntaxe de configuration de PF. On peut donc lui demander des choses de ce style :

root@spud[~]#tcpdump -ttt -i pflog0 inbound and action \

    pass and on wi0

Avec cette commande, il n'affichera que les paquets autorisés à passer, logués et entrants sur l'interface wi0.

Évidemment, tcpdump propose des dizaines d'options intéressantes, comme l'affichage hexadécimal des paquets filtrés, la reconnaissance de système d'exploitation, et bien d'autres encore. Pour toutes ces possibilités qui font mal au crâne, la page de manuel [14] sera la référence (qui a dit « comme d'habitude » ?).

Tiens, d'ailleurs, parlons vite fait de cette fameuse reconnaissance de système d'exploitation, aka « passive os fingerprinting ». PF utilise pour cela des parties de p0f [15], un outil écrit par Michal Zalewski, encore un hacker de génie venu des pays de l'Est.

Je ne m'étendrai pas sur ce sujet, sachez juste qu'il existe des techniques pointues d'analyse de paquets d'ouvertures de session TCP, de calcul de temps de réponse, de vaudou thaïlandais et de chamanisme à base d'incantations telles que « LE CODE EST LIBRE, LE CODE EST BEAU » ou « LA BOOOONNNE PAROOOOLE », tous ces gris-gris permettent d'avoir une bonne idée du système d'exploitation qu'utilise la machine avec laquelle on communique.

PF peut utiliser ces techniques à deux niveaux : on peut ajouter le mot-clé os à une règle de filtrage, auquel cas un paquet ne matchera la règle que si l'analyse du paquet laisse à penser que l'OS distant est celui que l'on désire filtrer. Exemple :

block in on $ext proto tcp from any \

    os {"Windows 95", "Windows 98"} to any port smtp

Cette règle permettra de bloquer un certain nombre de « spammeurs à l'insu de leur plein gré », pauvres machines tournant sous un OS bancal et vérolé jusqu'à la moelle. On peut obtenir la liste des systèmes d'exploitation que PF « reconnaît » avec la commande pfctl -so.

Enfin, on peut utiliser cette technique avec tcpdump, l'option -o permettra d'activer la détection de l'OS lors de la lecture d'un log.

root@spud[~]#tcpdump -o -r /var/log/pflog

08:57:29.102478 bas3-montreal02-1096688079.dsl.bell.ca.4996

    > my.network.org.54587: S (src OS: Windows XP SP1,

    Windows 2000 SP4) 1417722298:1417722298(0) win 65535

    <mss 1440,nop,nop,sackOK> (DF)

09:53:06.654419 host198-214.pool8248.interbusiness.it.3793

    > my.network.org.2967: S (src OS: Windows XP SP1,

    Windows 2000 SP2+) 2867956627:2867956627(0) win 16384

<mss 1460,nop,nop,sackOK> (DF)

Bien évidemment, ces techniques consomment du CPU, ne sont pas infaillibles, car on peut toujours bricoler les paramètres de sa pile TCP/IP pour essayer de se faire passer pour quelqu'un d'autre, mais dans ce domaine, p0f fait partie des meilleurs outils existants avec nmap. Si vous voulez en savoir plus, demandez « google os fingerprinting » à Pinpin.

6. A qui je demande MOOOOORE ?

6.1 Par ici la sortie, n'oubliez pas le guide

Voilà, j'espère avoir fait le tour de la question avec vous et que ce voyage au cœur d'un filtre de paquets réseau aura été intéressant. Vous devriez maintenant disposer des bases de PF, à vous d'en apprendre plus et d'utiliser ces connaissances à bon escient. Que la force soit avec vous, toussa !

Pour des choses un peu plus velues qui causent de haute disponibilité et d'équilibrage de charge, lisez maintenant la prose de messieurs *aigr*flab et ash-aire : ha_lb_bsd.

Linqses

- FAQ officielle PF

- Pf chez dhartmei@

- Pf par dhartmei@ sur http://undeadly.org/, #1

- Pf par dhartmei@ sur http://undeadly.org/, #2

- Pf par dhartmei@ sur http://undeadly.org/, #3

- PF sur wikipedia

- Tutoriel 1 : Peter Hansteen

- Tutoriel 2 : Peter Matulis

Bookin's

The OpenBSD PF Packet Filter Book

Building firewalls with OpenBSD and PF : références

[1] http:coombs.anu.edu.au/~avalon/

[2] http:marc.theaimsgroup.com/?l=ipfilter&m=99103632211327&w=2

[3] http://archives.neohapsis.com/archives/openbsd/2001-05/3215.html

[4] http:www.newsforge.com/article.pl?sid=01/06/06/169245

[5] http:archives.neohapsis.com/archives/openbsd/cvs/2001-05/1236.html

[6] http://kerneltrap.org/node/477

[7] http://www.onlamp.com/pub/a/bsd/2004/04/15/pf_developers.html

[8] http://www.onlamp.com/pub/a/bsd/2004/05/06/pf_developers.html

[9] http:marc.theaimsgroup.com/?t=105691343700002&r=2&w=2

[10] http:kerneltrap.org/node/627

[11] http://www.ietf.org/rfc/rfc2827.txt

[12] http://www.openbsd.org/cgi-bin/man.cgi?query=pf.conf

[13] http://www.openbsd.org/cgi-bin/man.cgi?query=pfctl

[14] http://www.openbsd.org/cgi-bin/man.cgi?query=tcpdump

[15] http://lcamtuf.coredump.cx/p0f.shtml