Packet Filter

Posted on August 21, 2014

J’ai présenté il y a peu iptables, mais j’ai envie de passer aux systèmes BSD depuis quelques temps. Et sur ces systèmes, le pare-feu se fait via le programme pf (Packet Filter). Rien à voir en terme de syntaxe, et c’est précisément ce qu’on va voir ici.

Cet article sera dédié à la création d’une passerelle utilisant pf. Toute la configuration présentée ici est basique et ne prend pas en compte des techniques avancées (utilisation de tables, gestion de files d’attente, redondance réseau avec CARP et pfsync…).

Cet article ne sera pas mis à jour, cependant la page dédiée à pf sur le site fera l’objet d’une mise à jour régulière, suivant mon propre apprentissage de l’outil.

Préliminaires

éléments de syntaxe

Le fichier pf.conf est plutôt bien documenté (man pf.conf, doc sur openbsd.org, livre The Book of PF ). Je vais reprendre ici quelques éléments présentés sur le site d’openbsd de façon succincte.

Toute la configuration du pare-feu se fait dans le fichier /etc/pf.conf. Le programme pf est actif au démarrage, mais on peut le désactiver en mettant PF=NO dans le fichier /etc/rc.local. Le contrôle du programme se fait via la commande pfctl.

Les éléments manipulés dans ce fichier seront :

  • des macro : adresse ou liste d’adresses ou plages IP, de ports, etc
  • des tables : une structure qui gère des listes d’adresses
  • des options : pour gérer comment fonctionne PF
  • des règles : pour filtrer les paquets, les rediriger, faire du NAT…

exemple de macro suivie d’une règle

int_if = "vio0"
pass on $int_if

Avec pass une commande pour laisser passer le paquet, et on $int_if ce qui arrive sur l’interface interne définie juste avant. Je n’explique pas plus, on verra par l’exemple.

en-tête du fichier

On va commencer par configurer quelques macros pour rendre le reste de la configuration le plus générique possible. N’oubliez pas que pour connaître les noms des interfaces disponibles on utilise ifconfig sous BSD.

# dépend de votre matériel
ext_if = "tun0" # interface externe (branchée à la box)
int_if = "vio0" # interface interne (branchée au réseau local)

# plage d'IP interne liée à votre interface interne
# (RFC 1918, généralement dans 192.168.0.0/16)
localnet = $int_if:network 

# ICMP autoriser les requêtes et les réponses unreach
icmp_types = "{ echoreq, unreach }"
pass inet proto icmp icmp-type $icmp_types

# tout ce qui sort sur l'interface externe (pour aller sur Internet)
# aura son adresse IP source remplacée par l'adresse IP de l'interface
# ext_if 
# on met des parenthèses autour du nom de l'interface si son adresse IP
# peut changer, auquel cas pf se débrouille pour ne pas perdre les
# connexions déjà établies
match out on $ext_if from $localnet nat-to ($ext_if)

# par défaut, on bloque TOUT
block all

Première phase : une passerelle et des clients

On a une passerelle entre la box de notre FAI et des clients (c’est-à-dire des ordinateurs sans serveur applicatif). On souhaite laisser la possibilité aux clients de sortir du réseau local. Bien entendu, on souhaite garder un certain contrôle sur ce qu’ils peuvent faire, si on a du windows à la maison, on a pas envie de devenir un nid à SPAM.

Autoriser les clients à sortir du réseau

On va se contenter de donner le droit aux clients de se connecter au web (http et https), à des machines distantes (ssh), à leur compte email (pop3, imap, imaps) et quelques autres trucs. Avec pf, nous n’avons pas besoin d’écrire les ports, nous pouvons faire référence à leur nom dans le fichier /etc/services.

client_out = "{ ssh, domain, imap, imaps, pop3, auth, \
              nntp, http, https, 6667 }"

# certains services utilisent aussi l'UDP
udp_services = "{ domain, ntp }"

# on laisse passer les paquets udp/tcp de façon inconditionnelle
pass quick inet proto { tcp, udp } to port $udp_services

# on laisse passer les connexions TCP autorisées
pass proto tcp to port $client_out

Seconde phase : ajout d’un serveur

Pour l’instant, on a notre passerelle et des clients. On va ajouter un serveur web à notre infra.

On va vouloir que notre passerelle redirige les connexions entrantes sur les ports 80 et 443 vers le serveur web interne.

Configuration du serveur web

On laisse passer ce qui arrive sur l’interface externe, et on redirige via le mot clé rdr-to vers l’adresse du serveur en interne. Il faudra laisser passer ensuite ce qui arrive sur l’interface interne vers le serveur web (aux ports associés).

webserver = "192.168.0.190"
webserverports = "{ http, https }"

# WEBSERVER
# redirection vers l'adresse IP local du serveur
pass in on $ext_if inet proto tcp to $ext_if port $webserverports rdr-to $webserver
# on laisse passer le trafic en direction du serveur (IP et port)
pass on $int_if inet proto tcp to $webserver port $webserverports

Cette configuration est suffisante pour un serveur web sans clients à l’intérieur du réseau local. Pour que les clients locaux puissent accéder au serveur web local, il faut détecter qu’on cherche à se connecter à l’IP de notre interface externe à partir d’une adresse locale. On redirige alors directement vers les ip des bons serveurs en local.

# connexion d'un client local à notre IP globale
# on le redirige vers le bon serveur en local
pass in on $int_if inet proto tcp from $localnet to $ext_if port $webserverports rdr-to $webserver

# NAT à l'IP interne pour ne pas que le serveur cherche à discuter 
# directement au client local mais passe par la passerelle
pass out on $int_if proto tcp from $localnet to $webserver port $webserverports nat-to $int_if

Troisième phase : serveur sans passerelle configurée

Cette troisième partie est utile voire nécessaire dans ma situation. L’explication est simple : ma passerelle OpenBSD est actuellement une passerelle entre un tunnel VPN et mon réseau local. Ce n’est pas ma passerelle pour accéder à internet directement. Je souhaite que tout le trafic de mes serveurs passe par le VPN, qui a une adresse IP fixe et qui est neutre, le reste passe par mon opérateur directement.

une histoire d’adresses IP source et destination

Actuellement, lors d’une connexion, on redirige le trafic entrant vers le serveur web, sans changer l’adresse IP source, juste celle de destination (IP publique => IP locale du serveur web). Le serveur web peut alors répondre directement à l’émetteur de la requête, et sa passerelle par défaut est mon serveur BSD (avec pf). Il comprend qu’il faut faire du NAT en sortie (changement de l’IP source de la réponse, celle du serveur web, par l’IP publique, de l’interface externe de la passerelle), tout se passe bien.

Le problème que j’ai actuellement est que sur un même ordinateur, je fais tourner un logiciel de partage en pair-à-pair, qui génère beaucoup de trafic, et un serveur de stockage de données Bacula (pour les sauvegardes). Je ne souhaite pas que mon trafic sur le VPN soit encombré, et donc je ne souhaite pas que le trafic du logiciel pair-à-pair sorte par le VPN.

une solution simple

La solution la plus simple à mettre en œuvre est de configurer la passerelle par défaut comme étant la box de l’opérateur, et de configurer pf pour qu’il fasse du NAT en interne. L’effet voulu est que la passerelle change l’adresse IP source des paquets entrants sur l’interface externe par l’adresse IP de son interface interne (comme fait précédemment pour les connexions « clients à serveur interne ») . Le logiciel serveur dans notre réseau local répondra alors à notre passerelle BSD.

serveur = "192.168.0.101"
serveurports = "{ 9103 }"

# comme pour l'autre serveur
pass in on $ext_if inet proto tcp to $ext_if port $serveurports rdr-to $serveur
pass out on $int_if inet proto tcp to $serveur port $serveurports
pass in on $int_if inet proto tcp from $localnet to $ext_if port  $serveurports rdr-to $serveur
pass out on $int_if proto tcp from $localnet to $serveur port $serveurports nat-to $int_if

# faire du NAT à la réception sur tout ce qui arrive en direction du serveur
pass out on $int_if proto tcp to $serveur port $serveurports received-on $ext_if nat-to $int_if
pass out on $int_if proto tcp to $serveur port $serveurports received-on $int_if nat-to $int_if

Quelques commandes utiles

Pour gérer PF

  • pfctl -e : activer pf
  • pfctl -d : désactiver pf

  • pfctl -f /etc/pf.conf : charger le fichier pf.conf
  • pfctl -nf /etc/pf.conf : vérifier que le fichier n’a pas d’erreur de syntaxe

  • pfctl -sr : montrer les règles courantes
  • pfctl -ss : montrer l’état des tables
  • pfctl -si : montrer les statistiques et compteurs de filtres
  • pfctl -sa : tout montrer

Pour lire les logs des règles

Les règles de pf qui sont historisées (via la règle log) peuvent être lues sur l’interface pflog0 via l’utilitaire tcpdump. Cet outil comprend les règles de BPF ainsi que la possibilité de sélectionner une règle pf à afficher.

exemple de lecture de log

tcpdump -i pflog0 # lire tous les paquets des règles historisées
tcpdump -i pflog0 port domain # lire les paquets port 53 (résolution DNS)

Chaque règle qui est inscrite dans votre fichier pf.conf est numérotée, ce qui a pour avantage de pouvoir être sélectionnée pour trier les logs.

log avec un numéro de règle pf

# règle historisée (mot clé : log)
pass log proto tcp to port $client_out

# chercher son numéro de règle
pfctl -vvs rules

# afficher les paquets qui correspondent à cette règle (X = numéro règle)
tcpdump -ttt -i pflog0 rulenum X

Les quelques options suivantes sont intéressantes à connaître pour le debug :

  • -ttt : permet d’afficher les dates de façon compréhensible
  • -n : ne pas faire de reverse dns, afficher les ip pas les noms de domaine
  • rulenum : pour spécifier un numéro de règle pf de votre pf.conf
Tags: bsd pf