21/11/2020
202011211200 ipfw shell
ipfw: modern FreeBSD (reloaded)
Après plusieurs mois de production, petit retour sur mon script de mur de feu.
Rappels
Ma configuration générale se trouve dans /etc/rc.conf.d et je n'ai pas de /etc/rc.conf. La configuration du mur de feu se trouve donc dans le répertoire /etc/rc.conf.d/ipfw. Jouer à distance avec un mur de feu peut vite devenir périlleux: une typo, une mauvaise règle et patatra le mur s'écroule, emportant avec lui la connexion ssh. Pour éviter ces désagréments:
- pas de règle dynamique pour le ssh
- uiliser le set 31
Plus d'explication dans mon premier billet où on trouvera aussi le pourquoi du modern FreeBSD. Attention, ce script ne convient pas à un hôte de type routeur.
Le script
Après quelques évolutions, j'arrive à ce résultat http://download.bsdsx.fr/rc.firewall:
$ cat -n /etc/rc.conf.d/rc.firewall 1 set -e 2 3 table_create() { 4 $fwcmd table $1 create type ${2:-addr} ${3:-} 5 } 6 7 table_load_addr() { 8 local table=$1 9 local files="" 10 local addrs="" 11 12 for arg in $@; do 13 case $arg in 14 /*) 15 [ -f $arg ] || continue 16 files="$files $arg" 17 ;; 18 [0-9a-fA-F]*) 19 addrs="$addrs $arg";; 20 esac 21 done 22 (echo $addrs; cat ${files:-/dev/null}) | sed -E -n -e 's/#.*//' -e 's/([[:xdigit:]:\.\/]+)/\1 0/gp' | tr ',' "\n" | sort --unique --ignore-leading-blanks | xargs $fwcmd table $table atomic add 23 } 24 25 set_31() { 26 [ ${firewall_debug} = 'NO' -a $($fwcmd set 31 list | wc -l) -gt 1 ] && return 27 28 table_create t_pouilleux 29 table_create t_ssh 30 31 table_load_addr t_ssh ${firewall_ssh6:-} ${firewall_ssh4:-} ${firewall_t_ssh:-} 32 table_load_addr t_pouilleux ${firewall_t_pouilleux:-} 33 34 $add set 31 check-state :KS 35 if $fwcmd table t_ssh info | grep -q 'items: 0'; then 36 $add set 31 deny in lookup src-ip t_pouilleux 37 $add set 31 pass { dst-port ssh or src-port ssh } 38 else 39 $add set 31 pass src-port ssh 40 $add set 31 pass in dst-port ssh lookup src-ip t_ssh 41 $add set 31 deny in dst-port ssh 42 $add set 31 deny in lookup src-ip t_pouilleux 43 fi 44 $add set 31 pass { proto ipv6-icmp or proto icmp } 45 } 46 . /etc/rc.subr 47 load_rc_config ipfw 48 49 set -u 50 51 [ $# -eq 1 ] && [ $1 = "-d" ] && firewall_debug=YES 52 53 fwcmd=/sbin/ipfw 54 case ${firewall_quiet:-} in 55 [Yy][Ee][Ss]) fwcmd="$fwcmd -q";; 56 esac 57 58 case ${firewall_debug:-NO} in 59 [Yy][Ee][Ss]) fwcmd="echo $fwcmd";; 60 *) firewall_debug='NO';; 61 esac 62 63 add="$fwcmd add" 64 65 set_31 66 67 KS="keep-state :KS" 68 69 $fwcmd -f flush 70 71 $add pass via lo* 72 73 [ -f $0.$(hostname) ] && . $0.$(hostname) 74 75 $add pass out $KS // after NAT 76 case ${firewall_logging:-} in 77 [Yy][Ee][Ss]) 78 for ports in ${firewall_nologports:-}; do 79 $add deny in dst-port $ports 80 done 81 ;; 82 esac 83 $add deny log in 84 85 # vim: set sw=4 sts=4 ts=4 ft=sh:
- 7-23: cette fonction me permet de charger une table de type addr à partir de variables et/ou de fichiers, les listes pouvant utiliser l'espace et/ou la virgule en tant que séparateur
- 25-45: définition du set 31 qui doit me permettre de garder ma connexion ssh en toute circonstance. Si le chargement de la table a échoué (ligne 35) alors l'accès ssh n'est pas restreint
- 51-61: debug / verbose
- 67: je précise le nom du flow des règles dynamiques, en prévision du nat
- 71: les interfaces loopback. A l'écriture de ce billet, je me demande si sa place ne serait pas dans le set 31 (au moins pour lo0, je verrais plus tard comment utiliser les jails et le nat sur lo1)
- 73: les règles de l'hôte. Utiliser le nom d'hôte me permet de centraliser/diffuser/copier sans risque ma configuration
- 75: le traffic sortant initié par l'hôte. Le commentaire a son importance: les futures règles de nat devront se placer avant celle-ci
- 76-82: traffic exempt de log uniquement en cas d'utilisation de syslog (pour éviter de le saturer)
Avec ce script et sans configuration je ne peux pas perdre l'accès ssh mais ce service est ouvert à tous les pouilleux, rabouins, peignes-cul et autres pénibles du nain ternet.
La configuration de base
$ cat -n /etc/rc.conf.d/ipfw/ipfw 1 firewall_enable="YES" 2 firewall_script="/etc/rc.conf.d/rc.firewall" 3 4 # logging == syslogd, logif == tcpdump 5 #firewall_logging="YES" 6 firewall_logif="YES" 7 8 ip6_dead="2001:db8:dead::/48" 9 ip6_beef="2001:db8:beef::/48" 10 ip6_cafe="2001:db8:cafe:cafe::/64" 11 12 ip4_maison="192.168.42.0/24 192.168.84.0/24, 10.10.0.0/16" 13 14 firewall_ssh6="$ip6_dead, $ip6_beef $ip6_cafe" 15 firewall_ssh4="$ip4_maison"
Pour tester et vérifier (ne pas hésiter à faire volontairement des typos dans les adresses et observer le résultat):
$ /bin/sh /etc/rc.conf.d/rc.firewall -d /sbin/ipfw table t_pouilleux create type addr /sbin/ipfw table t_ssh create type addr /sbin/ipfw table t_ssh atomic add 10.10.0.0/16 0 2001:db8:beef::/48 0 2001:db8:cafe:cafe::/64 0 192.168.42.0/24 0 192.168.84.0/24 0 2001:db8:dead::/48 0 /sbin/ipfw add set 31 check-state :KS /sbin/ipfw add set 31 pass src-port ssh /sbin/ipfw add set 31 pass in dst-port ssh lookup src-ip t_ssh /sbin/ipfw add set 31 deny in dst-port ssh /sbin/ipfw add set 31 deny in lookup src-ip t_pouilleux /sbin/ipfw add set 31 pass { proto ipv6-icmp or proto icmp } /sbin/ipfw -f flush /sbin/ipfw add pass via lo* /sbin/ipfw add pass out keep-state :KS // after NAT /sbin/ipfw add deny log in
Une configuration xen (non finalisée)
$ cat /etc/rc.conf.d/rc.firewall.xen.bsdsx.fr table_create t_unbound addr or-flush && table_load_addr t_unbound ${firewall_unbound6:-} ${firewall_unbound4:-} ${firewall_t_unbound:-} $add pass in dst-port domain lookup src-ip t_unbound $KS $add pass in dst-port http $KS $add pass in src-port bootpc via xnb* $add pass in dst-port ntp,syslog,546 via xnb* $add pass in src-port bootps via vlan22
Cette machine fournit un service dns à accès restreint, un service http non restreint et laisse passer du traffic en provenance/destination des domU (j'y reviendrais certainement dans un autre billet).
A venir
J'ai abandonné l'idée de mur de feu entièrement pilotable à coup de variables, le code nécessaire pour définir un service était beaucoup trop volumineux. L'utilisation d'un script local à l'hôte me parait plus simple. La prochaine évolution concernera le nat et des règles qui, je l'espère, sortiront des sentiers battus.
Commentaires: https://github.com/bsdsx/blog_posts/issues/6
25/01/2020
202001251100 ipfw shell
ipfw: modern FreeBSD, services
Pour illustrer mon propos, il me faut un service. Un truc simple qui fasse udp/tcp et ipv6/ipv4. J'aurais pu utiliser nc -k -l 1234 mais il n'est pas possible de faire tcp/udp/ipv6/ipv4 en même temps (sauf à lancer 4 nc). J'ai donc jeter mon dévolu sur daytime fourni par inetd:
$ sed -n '/#daytime/s/^.//p' /etc/inetd.conf > /tmp/inetd.conf $ sudo /usr/sbin/inetd -d -l /tmp/inetd.conf [ snip du debug ]
Depuis la même machine, je peux m'y connecter en tcp:
$ nc -d -6 localhost 13 $ nc -d -4 localhost 13
et vérifier l'udp:
$ nc -z -u -6 localhost 13 $ nc -z -u -4 localhost 13
On doit voir une trace de ces connexions dans la console où est lancé inetd (je ne sais pas pourquoi l'udp génère 4 lignes de debug). Toute tentative depuis une machine distante se soldera par un échec car l'accès au port "13" n'est pas autorisé.
Service
J'aime assez le concept de mur de feu piloté par des variables. Mon service ssh est géré par les variables suivantes:
- firewall_ssh6
- firewall_ssh4
- firewall_t_ssh
Le nommage de ces variables suit cette règle: 'firewall' + nom_du_service + 4|6 ou 'firewall' + 't' + nom_du_service. Cette règle à (au moins) 2 défauts:
- pas de distinction udp/tcp
- les variables utilisent le nom du service (ssh) alors que le script utilise le numéro de port associé (22)
Pour simplifier mes règles, je pars du principe qu'un service est à la fois tcp et udp. Pour les noms/numéros de port je peux utiliser les noms dans le script mais comment définir un service qui écoute sur le port 3128 ? Ou pire, sur plusieurs ports ? Voici ce que je voudrais pouvoir faire:
1 firewall_services="proxy http,dns ,foo bar, baz ,, , , mail_int daytime" 2 3 firewall_proxy_port=3128 4 firewall_proxy4="192.168.0.0/24 172.16.0.0/24, 172.16.2.0/24" 5 6 firewall_dns_ports=",53,853, 8853" 7 8 firewall_foo_ports=9000 9 firewall_t_foo=t_bar 10 11 firewall_bar_port="9001 9002" 12 firewall_t_bar=/etc/rc.conf.d/bar.* 13 14 firewall_baz_port=9003 15 firewall_t_baz=t_bar 16 17 firewall_mail_int_ports="smtp 587 imap 993 4190" 18 firewall_t_mail_int=t_ssh
- 1: je définis une liste de service (on portera une attention particulière aux services "http" et "daytime")
- 3-4: un service "proxy" écoute sur le port "3128" en mode "restreint" à partir d'une liste (qui sera transformée en table)
- 6: un service "dns" écoute sur les ports "53", "853" et "8853" en mode "ouvert aux 4 vents"
- 8-9: un service "foo" écoute sur le port "9000" en mode "restreint" en utilisant la table "t_bar" qui sera définie plus tard
- 11-12: un service "bar" écoute sur les ports "9001" et "9002" en mode "restreint" en utilisant sa table "t_bar" qui sera chargée à partir de fichiers
- 14-15: un service "baz" écoute sur le port "9003" en mode "restreint" en utilisant la table "t_bar" définie plus tôt
- 17-18: un service "mail_int" écoute sur des ports qui semblent concerner le courriel (smtp, submission, imap, imaps et sieve) en mode "restreint" en utilisant la table "t_ssh"
Sans configuration particulière, les services "http" et "daytime" écoutent sur leur port respectif en mode "ouvert aux 4 vents".
Je veux aussi pouvoir traiter les gros doigts ("port" vs "ports") et les listes ("1 2 3" vs "a,b, c ,, d").
Code
60 set_31 61 $fwcmd -f flush 62 63 $add pass via lo0 64 $add pass out keep-state 65 66 for svc in $(echo ${firewall_services:-} | tr ',' ' '); do 67 eval "port=\"\${firewall_${svc}_ports:-\${firewall_${svc}_port:-$svc}}\"" 68 [ "$port" = $svc ] || port=$(echo $port | sed -E -e 's/[ ,]+/,/g' -e 's/(^,|,$)//') 69 70 eval "table=\"\${firewall_t_${svc}:-}\"" 71 case "$table" in 72 t_*) [ $table = "t_$svc" ] || $add pass in dst-port $port lookup src-ip $table keep-state; continue;; 73 esac 74 75 eval "table_src=\"\${firewall_${svc}6:-} \${firewall_${svc}4:-} \${firewall_t_${svc}:-}\"" 76 if [ "$table_src" = " " ]; then 77 $add pass in dst-port $port keep-state 78 continue 79 fi 80 81 $fwcmd table t_$svc create type addr or-flush 82 load_table_addr t_$svc $table_src 83 $add pass in dst-port $port lookup src-ip t_$svc keep-state 84 done 85 86 $add pass { proto ipv6-icmp or proto icmp } 87 $add deny log in
- 66: je normalise le contenu de la variable firewall_services à l'aide de tr
- 67: l'échappement de nombreux caractères ne facilite pas la lecture: définir une variable port à partir de {firewall_svc_ports:-{firewall_svc_port:-svc}}
- 68: je normalise le contenu de port s'il est différent de svc
- 70-73: si mon service utilise une table et que le nom de cette table ne correspond pas à t_service alors j'ajoute une règle en mode "restreint" + règle dynamique et je passe au service suivant
- 75-79: si mon service n'est pas en mode "restreint" (les variables firewall_svc(6|4) et firewall_t_svc sont vides) alors j'ajoute une règle en mode "ouvert aux 4 vents" + règle dynamique et je passe au service suivant
- 81-83: je crée une table correspondant au service (ou je la vide si elle existe déjà: or-flush), je la charge et j'ajoute une règle en mode "restreint" + règle dynamique
Au final
1 $ sudo service ipfw restart 2 Flushed all rules. 3 00500 allow via lo0 4 00000 allow out keep-state :default 5 added: 172.16.2.0/24 0 6 added: 192.168.0.0/24 0 7 added: 172.16.0.0/24 0 8 00000 allow in dst-port 3128 dst-ip lookup src-ip t_proxy keep-state :default 9 00000 allow in dst-port 80 keep-state :default 10 00000 allow in dst-port 53,853,8853 keep-state :default 11 00000 allow in dst-port 9000 dst-ip lookup src-ip t_bar keep-state :default 12 00000 allow in dst-port 9001,9002 dst-ip lookup src-ip t_bar keep-state :default 13 00000 allow in dst-port 9003 dst-ip lookup src-ip t_bar keep-state :default 14 00000 allow in dst-port 25,587,143,993,4190 dst-ip lookup src-ip t_ssh keep-state :default 15 00000 allow in dst-port 13 keep-state :default 16 01500 allow { proto ipv6-icmp or proto icmp } 17 01600 deny log in 18 Firewall rules loaded. 19 Firewall logging enabled.
On retrouve bien les règles définies au paragraphe "Service". Je pense que 'dst-ip' n'est qu'un bug d'affichage.
Si depuis une machine distante je teste le port "13" en udp ("nc -z -u -4 ip.de.la.machine 13") je peux voir qu'une règle dynamique a bien été ajoutée:
$ sudo ipfw -d -S show | grep ' 13 ' 01400 8 332 set 0 allow in dst-port 13 keep-state :default 01400 8 332 (9s) STATE udp 172.16.200.193 10930 <-> 172.16.100.50 13 :default
C'est un peu plus compliqué avec le tcp car la règle est supprimée dès que la connexion est terminée (et elle se termine très vite :).
01400 7 406 (1s) STATE tcp 172.16.200.193 24467 <-> 172.16.100.50 13 :default
Commentaires: https://github.com/bsdsx/blog_posts/issues/3
10/01/2020
202001101600 ipfw shell
ipfw: modern FreeBSD, les tables
Les tables permettent de faire des listes de différents types:
- adresse (ipv6/ipv4 et masque de réseau optionnel)
- nombre (port, uid/gid, jail id)
- nom d'interface
- flow
C'est l'outil idéal pour bloquer ce que je nomme pudiquement les pouilleux:
$fwcmd table t_pouilleux create type addr ... $fwcmd add deny in lookup src-ip t_pouilleux
Pour ajouter/retirer une adresse, rien de plus simple:
# ipfw table t_pouilleux add 8.8.8.8 added: 8.8.8.8/32 0 # ipfw table t_pouilleux delete 8.8.8.8 deleted: 8.8.8.8/32 0
On peut aussi utiliser des noms d'hôtes qui seront résolus lors de l'ajout. Mais attention, si cette résolution retourne plusieurs adresses (ipv4 et/ou ipv6) elles ne seront pas toutes intégrées. Je pars donc du principe que ce n'est pas une bonne idée et qu'il faut utiliser uniquement des adresses.
La purge
Par défaut, chaque entrée possède une valeur (de type legacy) qui vaut 0. On peut donc ajouter une information temporelle dans l'objectif de purger régulièrement la table:
# ipfw table t_pouilleux flush # ipfw table t_pouilleux add 8.8.8.8 `date '+%s'` added: 8.8.8.8/32 1577471400 # ipfw table t_pouilleux add 8.8.4.4 1577385000 added: 8.8.4.4/32 1577385000
Le script suivant supprime les entrées d'une table dont la valeur est inférieure à un nombre de secondes:
1 #!/bin/sh 2 3 set -eu 4 5 nb_days=15 6 table=t_pouilleux 7 dbg= 8 quiet= 9 10 usage() { 11 echo "Usage: ${0##*/} [-n nb_days] [-t table] [-d] [-q] [-h] 12 -n nb_days default $nb_days 13 -t table default $table 14 -d debug mode 15 -q quiet mode 16 -h this message 17 " 18 } 19 20 show_error() { echo $msg_error'. Exit'; } 21 22 args=$(getopt dqhn:t: $*) 23 24 if [ $? -ne 0 ]; then 25 usage 26 exit 2 27 fi 28 set -- $args 29 while :; do 30 case "$1" in 31 -d) dbg=echo; shift;; 32 -q) quiet=$1; shift;; 33 -n) nb_days=$2; shift; shift;; 34 -t) table=$2; shift; shift;; 35 -h) usage; exit 0;; 36 --) shift; break;; 37 esac 38 done 39 40 trap show_error 0 41 msg_error="table '$table': not found" 42 ipfw table all list | grep --quiet "table($table)" 43 msg_error="table '$table': not type addr" 44 ipfw table $table info | grep --quiet 'type: addr' 45 trap "" 0 46 47 max_seconds=$(($(date '+%s') - 86400 * $nb_days)) 48 49 ipfw table $table list | while read addr seconds; do 50 [ ${seconds:-0} -ne 0 -a $seconds -lt $max_seconds ] && $dbg ipfw $quiet table $table delete $addr 51 done
- 1-38: variables, options, usage (soit les 3/4 du script)
- 40-44: l'utilisation combiné de set -e et d'un pipe m'oblige à intercepter les erreurs (trap). Il faut définir le message d'erreur avant que l'éventuelle erreur se produise.
- 45: fin d'interception des erreurs
- 49-51: le coeur du script
Table ou pas table ?
On peut imaginer remplacer les variables firewall_ssh6 et firewall_ssh4 (voir mon billet précédent) par une table.
Pour:
- une seule règle au lieu de 2
- ajout d'entrées à la demande (mais ce point peut être discutable: dans la majorité des cas, les accès ssh sont définis une bonne fois pour toute)
- simplification dans le cas de liste à rallonge
Contre:
- code supplémentaire pour charger la table
Si mes adresses sont dans un (ou des) fichier(s) avec la syntaxe suivante:
# commentaire 8.8.8.8 # autre commentaire avec des espaces inutiles 2001:4860:4860::8888 2002::/8 # espaces inutiles et commentaire 1.1.1.1, 2.2.2.2/2 3.3.3.3/32, 4.4.4.4/24 #plusieurs adresses par ligne avec ou sans virgule
je dois, pour que ces données soient compréhensibles par ipfw:
- supprimer les commentaires
- afficher ce qui ressemble à une adresse en la faisant suivre par ' 0,'
- transformer les ',' en "\n"
Ce qui peut donner (les améliorations dans les commentaires sont les bienvenues :):
# sed -E -n -e 's/#.*//' -e 's/([[:xdigit:]:\.\/]+)/\1 0,/gp' fichier(s) | tr ',' "\n" | xargs ipfw table t_ssh add
Je laisse le soin aux gourous de sed d'affiner mon motif qui doit leur paraitre bien naïf. Quant aux furieux d'ipfw, ils n'hésiteront pas à ajouter l'option atomic dans un pur esprit "tout ou rien". Et cerise sur le gâteau, xargs a le bon goût de ne rien faire si aucun argument n'est fourni.
J'enrobe le tout dans une fonction load_table_addr et ma table t_ssh est chargée depuis un ou des fichiers. Et pourquoi pas charger t_pouilleux avec la liste des bogons ? Mais il va se poser 2 problèmes:
- sed génère une erreur s'il ne peut pas accéder à un fichier
- ipfw sans -q génère une erreur en cas de doublon d'adresse
Ces erreurs seront interceptées par set -e et le script se terminera prématurément, laissant notre mur de feu dans un état proche de l'Ohio (dixit Isabelle A.).
Le cas "sed" peut se gérer avec une boucle:
for file in $files; do [ -f $file ] || continue sed ... $file | tr ',' "\n" | xargs $fwcmd ... done
mais on exécute la commande pour chaque fichier. Autant faire une liste de fichier existant (et au passage tester cette liste):
local files_ok="" for file in $files; do [ -f $file ] || continue $files_ok="$files_ok $file" done [ -n "$files_ok" ] || return sed ... $files_ok | tr ',' "\n" | xargs $fwcmd ...
Le cas des doublons d'adresse est tout aussi simple:
sed ... $files_ok | tr ',' "\n" | sort --unique --ignore-leading-blanks | xargs $fwcmd ...
Une fonction pour tous les charger
1 load_table_addr() { 2 local table=$1; shift 3 local files="" 4 for file in $@; do 5 [ -f $file ] || continue 6 files="$files $file" 7 done 8 [ -n "$files" ] || return 9 sed -E -n -e 's/#.*//g' -e 's/([[:xdigit:]:\.\/]+)/\1 0,/gp' $files | tr ',' "\n" | sort --unique --ignore-leading-blanks | xargs $fwcmd table $table add 10 }
Les tests des lignes 5 et 8 méritent sans doute un message d'alerte.
Fichiers ou variables ?
Si je veux gérer les doublons tout en utilisant le contenu de variables *ET* de fichiers je dois ruser.
1 load_table_addr() { 2 [ $# -gt 1 ] || return 3 local table=$1 4 local files="" 5 local addrs="" 6 7 for arg in $@; do 8 case $arg in 9 /*) 10 [ -f $arg ] || continue 11 files="$files $arg" 12 ;; 13 [0-9a-fA-F]*) 14 addrs="$addrs $args";; 15 esac 16 done 17 (echo $addrs; cat ${files:-/dev/null}) | sed -E -n -e 's/#.*//g' -e 's/([[:xdigit:]:\.\/]+).*/\1 0/gp' | tr ',' "\n" | sort --unique --ignore-leading-blanks | xargs $fwcmd table $table atomic add 18 }
- 2: pas la peine de continuer si aucun autre argument que le nom de la table
- 4-5: deux listes: fichiers et adresses
- 9-12: l'argument correspond à un nom de fichier (nom absolu donc commence par '/')
- 13-15: l'argument correspond à une adresse (grosso merdo)
- 17: j'affiche addrs puis le contenu des fichiers correspondant à files (qui au pire vaut /dev/null) le tout dans un subshell dont la sortie est redirigée vers le reste de mes commandes. Y'a pas à dire: le shell, c'est bô.
Alors fichiers ou variables ? Les deux mon Général !
Pour finir
Le chargement de mes tables est grandement simplifié:
load_table_addr t_ssh ${firewall_ssh6:-} ${firewall_ssh4:-} ${firewall_t_ssh:-} load_table_addr t_pouilleux ${firewall_t_pouilleux:-}
Tout comme les règles concernant le ssh: je charge la table t_ssh et si elle est vide je passe en mode "ouvert aux 4 vents".
$ cat -n /etc/rc.conf.d/rc.firewall 1 set -e 2 3 load_table_addr() { 4 local table=$1 5 local files="" 6 local addrs="" 7 8 for arg in $@; do 9 case $arg in 10 /*) 11 [ -f $arg ] || continue 12 files="$files $arg" 13 ;; 14 [0-9a-fA-F]*) 15 addrs="$addrs $arg";; 16 esac 17 done 18 (echo $addrs; cat ${files:-/dev/null}) | sed -E -n -e 's/#.*//' -e 's/([[:xdigit:]:\.\/]+)/\1 0/gp' | tr ',' "\n" | sort --unique --ignore-leading-blanks | xargs $fwcmd table $table atomic add 19 } 20 21 set_31() { 22 [ ${firewall_debug} = 'NO' -a $($fwcmd set 31 list | wc -l) -gt 1 ] && return 23 24 $fwcmd table t_pouilleux create type addr 25 $fwcmd table t_ssh create type addr 26 27 load_table_addr t_ssh ${firewall_ssh6:-} ${firewall_ssh4:-} ${firewall_t_ssh:-} 28 load_table_addr t_pouilleux ${firewall_t_pouilleux:-} 29 30 $add set 31 check-state 31 if $fwcmd table t_ssh info | grep -q 'items: 0'; then 32 $add set 31 deny in lookup src-ip t_pouilleux 33 $add set 31 pass { dst-port 22 or src-port 22 } 34 else 35 $add set 31 pass src-port 22 36 $add set 31 pass in dst-port 22 lookup src-ip t_ssh 37 $add set 31 deny in lookup src-ip t_pouilleux 38 fi 39 } 40 41 . /etc/rc.subr 42 load_rc_config ipfw 43 44 set -u 45 46 [ $# -eq 1 ] && [ $1 = "-d" ] && firewall_debug=YES 47 48 fwcmd=/sbin/ipfw 49 case ${firewall_quiet:-} in 50 [Yy][Ee][Ss]) fwcmd="$fwcmd -q";; 51 esac 52 53 case ${firewall_debug:-NO} in 54 [Yy][Ee][Ss]) fwcmd="echo $fwcmd";; 55 *) firewall_debug='NO';; 56 esac 57 58 add="$fwcmd add" 59 60 set_31 61 $fwcmd -f flush 62 63 $add pass via lo0 64 $add pass out keep-state 65 $add pass { proto ipv6-icmp or proto icmp } 66 $add deny log in
Quelques remarques:
- en mode debug le test de la ligne 31 sera toujours faux
- en mode "ouvert aux 4 vents" le trafic "ssh" ne génèrera aucune règle dynamique (ligne 33). Ce n'est pas le cas en mode "ssh restreint": le premier paquet d'une connexion initiée par la machine (out, dst-port 22) ne correspond pas aux lignes 35-36 mais à la ligne 64 et génère une règle dynamique. Je ne pense pas que ce soit un problème car quand je me connecte sur une machine et que je joue avec les règles du mur de feu, c'est *MA* connexion qui ne doit pas être coupée et la susdite machine va rarement initier du ssh dans mon dos (aka cron) en même temps.
- le contenu de mes règles suit l'ordre suivant: drapeau - comparaison - recherche
Commentaires: https://github.com/bsdsx/blog_posts/issues/2
10/01/2020
202001101600 ipfw shell
ipfw: modern FreeBSD, les tables
Les tables permettent de faire des listes de différents types:
- adresse (ipv6/ipv4 et masque de réseau optionnel)
- nombre (port, uid/gid, jail id)
- nom d'interface
- flow
C'est l'outil idéal pour bloquer ce que je nomme pudiquement les pouilleux:
$fwcmd table t_pouilleux create type addr ... $fwcmd add deny in lookup src-ip t_pouilleux
Pour ajouter/retirer une adresse, rien de plus simple:
# ipfw table t_pouilleux add 8.8.8.8 added: 8.8.8.8/32 0 # ipfw table t_pouilleux delete 8.8.8.8 deleted: 8.8.8.8/32 0
On peut aussi utiliser des noms d'hôtes qui seront résolus lors de l'ajout. Mais attention, si cette résolution retourne plusieurs adresses (ipv4 et/ou ipv6) elles ne seront pas toutes intégrées. Je pars donc du principe que ce n'est pas une bonne idée et qu'il faut utiliser uniquement des adresses.
La purge
Par défaut, chaque entrée possède une valeur (de type legacy) qui vaut 0. On peut donc ajouter une information temporelle dans l'objectif de purger régulièrement la table:
# ipfw table t_pouilleux flush # ipfw table t_pouilleux add 8.8.8.8 `date '+%s'` added: 8.8.8.8/32 1577471400 # ipfw table t_pouilleux add 8.8.4.4 1577385000 added: 8.8.4.4/32 1577385000
Le script suivant supprime les entrées d'une table dont la valeur est inférieure à un nombre de secondes:
1 #!/bin/sh 2 3 set -eu 4 5 nb_days=15 6 table=t_pouilleux 7 dbg= 8 quiet= 9 10 usage() { 11 echo "Usage: ${0##*/} [-n nb_days] [-t table] [-d] [-q] [-h] 12 -n nb_days default $nb_days 13 -t table default $table 14 -d debug mode 15 -q quiet mode 16 -h this message 17 " 18 } 19 20 show_error() { echo $msg_error'. Exit'; } 21 22 args=$(getopt dqhn:t: $*) 23 24 if [ $? -ne 0 ]; then 25 usage 26 exit 2 27 fi 28 set -- $args 29 while :; do 30 case "$1" in 31 -d) dbg=echo; shift;; 32 -q) quiet=$1; shift;; 33 -n) nb_days=$2; shift; shift;; 34 -t) table=$2; shift; shift;; 35 -h) usage; exit 0;; 36 --) shift; break;; 37 esac 38 done 39 40 trap show_error 0 41 msg_error="table '$table': not found" 42 ipfw table all list | grep --quiet "table($table)" 43 msg_error="table '$table': not type addr" 44 ipfw table $table info | grep --quiet 'type: addr' 45 trap "" 0 46 47 max_seconds=$(($(date '+%s') - 86400 * $nb_days)) 48 49 ipfw table $table list | while read addr seconds; do 50 [ ${seconds:-0} -ne 0 -a $seconds -lt $max_seconds ] && $dbg ipfw $quiet table $table delete $addr 51 done
- 1-38: variables, options, usage (soit les 3/4 du script)
- 40-44: l'utilisation combiné de set -e et d'un pipe m'oblige à intercepter les erreurs (trap). Il faut définir le message d'erreur avant que l'éventuelle erreur se produise.
- 45: fin d'interception des erreurs
- 49-51: le coeur du script
Table ou pas table ?
On peut imaginer remplacer les variables firewall_ssh6 et firewall_ssh4 (voir mon billet précédent) par une table.
Pour:
- une seule règle au lieu de 2
- ajout d'entrées à la demande (mais ce point peut être discutable: dans la majorité des cas, les accès ssh sont définis une bonne fois pour toute)
- simplification dans le cas de liste à rallonge
Contre:
- code supplémentaire pour charger la table
Si mes adresses sont dans un (ou des) fichier(s) avec la syntaxe suivante:
# commentaire 8.8.8.8 # autre commentaire avec des espaces inutiles 2001:4860:4860::8888 2002::/8 # espaces inutiles et commentaire 1.1.1.1, 2.2.2.2/2 3.3.3.3/32, 4.4.4.4/24 #plusieurs adresses par ligne avec ou sans virgule
je dois, pour que ces données soient compréhensibles par ipfw:
- supprimer les commentaires
- afficher ce qui ressemble à une adresse en la faisant suivre par ' 0,'
- transformer les ',' en "\n"
Ce qui peut donner (les améliorations dans les commentaires sont les bienvenues :):
# sed -E -n -e 's/#.*//' -e 's/([[:xdigit:]:\.\/]+)/\1 0,/gp' fichier(s) | tr ',' "\n" | xargs ipfw table t_ssh add
Je laisse le soin aux gourous de sed d'affiner mon motif qui doit leur paraitre bien naïf. Quant aux furieux d'ipfw, ils n'hésiteront pas à ajouter l'option atomic dans un pur esprit "tout ou rien". Et cerise sur le gâteau, xargs a le bon goût de ne rien faire si aucun argument n'est fourni.
J'enrobe le tout dans une fonction load_table_addr et ma table t_ssh est chargée depuis un ou des fichiers. Et pourquoi pas charger t_pouilleux avec la liste des bogons ? Mais il va se poser 2 problèmes:
- sed génère une erreur s'il ne peut pas accéder à un fichier
- ipfw sans -q génère une erreur en cas de doublon d'adresse
Ces erreurs seront interceptées par set -e et le script se terminera prématurément, laissant notre mur de feu dans un état proche de l'Ohio (dixit Isabelle A.).
Le cas "sed" peut se gérer avec une boucle:
for file in $files; do [ -f $file ] || continue sed ... $file | tr ',' "\n" | xargs $fwcmd ... done
mais on exécute la commande pour chaque fichier. Autant faire une liste de fichier existant (et au passage tester cette liste):
local files_ok="" for file in $files; do [ -f $file ] || continue $files_ok="$files_ok $file" done [ -n "$files_ok" ] || return sed ... $files_ok | tr ',' "\n" | xargs $fwcmd ...
Le cas des doublons d'adresse est tout aussi simple:
sed ... $files_ok | tr ',' "\n" | sort --unique --ignore-leading-blanks | xargs $fwcmd ...
Une fonction pour tous les charger
1 load_table_addr() { 2 local table=$1; shift 3 local files="" 4 for file in $@; do 5 [ -f $file ] || continue 6 files="$files $file" 7 done 8 [ -n "$files" ] || return 9 sed -E -n -e 's/#.*//g' -e 's/([[:xdigit:]:\.\/]+)/\1 0,/gp' $files | tr ',' "\n" | sort --unique --ignore-leading-blanks | xargs $fwcmd table $table add 10 }
Les tests des lignes 5 et 8 méritent sans doute un message d'alerte.
Fichiers ou variables ?
Si je veux gérer les doublons tout en utilisant le contenu de variables *ET* de fichiers je dois ruser.
1 load_table_addr() { 2 [ $# -gt 1 ] || return 3 local table=$1 4 local files="" 5 local addrs="" 6 7 for arg in $@; do 8 case $arg in 9 /*) 10 [ -f $arg ] || continue 11 files="$files $arg" 12 ;; 13 [0-9a-fA-F]*) 14 addrs="$addrs $args";; 15 esac 16 done 17 (echo $addrs; cat ${files:-/dev/null}) | sed -E -n -e 's/#.*//g' -e 's/([[:xdigit:]:\.\/]+).*/\1 0/gp' | tr ',' "\n" | sort --unique --ignore-leading-blanks | xargs $fwcmd table $table atomic add 18 }
- 2: pas la peine de continuer si aucun autre argument que le nom de la table
- 4-5: deux listes: fichiers et adresses
- 9-12: l'argument correspond à un nom de fichier (nom absolu donc commence par '/')
- 13-15: l'argument correspond à une adresse (grosso merdo)
- 17: j'affiche addrs puis le contenu des fichiers correspondant à files (qui au pire vaut /dev/null) le tout dans un subshell dont la sortie est redirigée vers le reste de mes commandes. Y'a pas à dire: le shell, c'est bô.
Alors fichiers ou variables ? Les deux mon Général !
Pour finir
Le chargement de mes tables est grandement simplifié:
load_table_addr t_ssh ${firewall_ssh6:-} ${firewall_ssh4:-} ${firewall_t_ssh:-} load_table_addr t_pouilleux ${firewall_t_pouilleux:-}
Tout comme les règles concernant le ssh: je charge la table t_ssh et si elle est vide je passe en mode "ouvert aux 4 vents".
$ cat -n /etc/rc.conf.d/rc.firewall 1 set -e 2 3 load_table_addr() { 4 local table=$1 5 local files="" 6 local addrs="" 7 8 for arg in $@; do 9 case $arg in 10 /*) 11 [ -f $arg ] || continue 12 files="$files $arg" 13 ;; 14 [0-9a-fA-F]*) 15 addrs="$addrs $arg";; 16 esac 17 done 18 (echo $addrs; cat ${files:-/dev/null}) | sed -E -n -e 's/#.*//' -e 's/([[:xdigit:]:\.\/]+)/\1 0/gp' | tr ',' "\n" | sort --unique --ignore-leading-blanks | xargs $fwcmd table $table atomic add 19 } 20 21 set_31() { 22 [ ${firewall_debug} = 'NO' -a $($fwcmd set 31 list | wc -l) -gt 1 ] && return 23 24 $fwcmd table t_pouilleux create type addr 25 $fwcmd table t_ssh create type addr 26 27 load_table_addr t_ssh ${firewall_ssh6:-} ${firewall_ssh4:-} ${firewall_t_ssh:-} 28 load_table_addr t_pouilleux ${firewall_t_pouilleux:-} 29 30 $add set 31 check-state 31 if $fwcmd table t_ssh info | grep -q 'items: 0'; then 32 $add set 31 deny in lookup src-ip t_pouilleux 33 $add set 31 pass { dst-port 22 or src-port 22 } 34 else 35 $add set 31 pass src-port 22 36 $add set 31 pass in dst-port 22 lookup src-ip t_ssh 37 $add set 31 deny in lookup src-ip t_pouilleux 38 fi 39 } 40 41 . /etc/rc.subr 42 load_rc_config ipfw 43 44 set -u 45 46 [ $# -eq 1 ] && [ $1 = "-d" ] && firewall_debug=YES 47 48 fwcmd=/sbin/ipfw 49 case ${firewall_quiet:-} in 50 [Yy][Ee][Ss]) fwcmd="$fwcmd -q";; 51 esac 52 53 case ${firewall_debug:-NO} in 54 [Yy][Ee][Ss]) fwcmd="echo $fwcmd";; 55 *) firewall_debug='NO';; 56 esac 57 58 add="$fwcmd add" 59 60 set_31 61 $fwcmd -f flush 62 63 $add pass via lo0 64 $add pass out keep-state 65 $add pass { proto ipv6-icmp or proto icmp } 66 $add deny log in
Quelques remarques:
- en mode debug le test de la ligne 31 sera toujours faux
- en mode "ouvert aux 4 vents" le trafic "ssh" ne génèrera aucune règle dynamique (ligne 33). Ce n'est pas le cas en mode "ssh restreint": le premier paquet d'une connexion initiée par la machine (out, dst-port 22) ne correspond pas aux lignes 35-36 mais à la ligne 64 et génère une règle dynamique. Je ne pense pas que ce soit un problème car quand je me connecte sur une machine et que je joue avec les règles du mur de feu, c'est *MA* connexion qui ne doit pas être coupée et la susdite machine va rarement initier du ssh dans mon dos (aka cron) en même temps.
- le contenu de mes règles suit l'ordre suivant: drapeau - comparaison - recherche
Commentaires: https://github.com/bsdsx/blog_posts/issues/2
07/12/2019
201912070800 ipfw shell
ipfw: modern FreeBSD
C'est pas moi qui le dit:
$ man ipfw ... RULE BODY ... The rule body has the following format:
[proto from src to dst] [options]
The first part (proto from src to dst) is for backward compatibility with earlier versions of FreeBSD. In modern FreeBSD any match pattern (including MAC headers, IP protocols, addresses and ports) can be specified in the options section.
Mes recherches de fichiers de règles basées sur la syntaxe [options] ne m'ayant pas donné satisfaction, j'ai décidé de dégainer mon $EDITOR. Mais avant de parler code, parlons contexte et objectifs:
- une machine avec 1 interface réseau
- ssh toujours actif, même en cas de rechargement du service
- syntaxe "options"
- ipv4 et ipv6
- debug facile
Configuration
Je n'utilise pas le fichier /etc/rc.conf, je lui préfère le répertoire /etc/rc.conf.d. Mon script de règles sera donc /etc/rc.conf.d/rc.firewall et mes variables seront réparties dans des fichiers placés dans /etc/rc.conf.d/ipfw/.
Version initiale
Je commence par ces quelques lignes:
$ cat -n /etc/rc.conf.d/rc.firewall 1 set -e 2 3 . /etc/rc.subr 4 load_rc_config ipfw 5 6 set -u 7 8 [ $# -eq 1 ] && [ $1 = "-d" ] && firewall_debug=YES 9 10 fwcmd=/sbin/ipfw 11 case ${firewall_quiet:-} in 12 [Yy][Ee][Ss]) fwcmd="$fwcmd -q";; 13 esac 14 15 case ${firewall_debug:-NO} in 16 [Yy][Ee][Ss]) fwcmd="echo $fwcmd";; 17 *) firewall_debug='NO';; 18 esac 19 20 $fwcmd -f flush
- 1: gstion des erreurs
- 3-4: chargement de la configuration du service ipfw
- 6: gestions des variables non définies (incompatible avec le chargement de configuration de service)
- 8: mode debug
- 11-13: mode verbeux
- 15-18: mode debug + mode trace (ligne 16)
- 20: suppression des règles courantes
On peut déjà utiliser le script:
$ /bin/sh /etc/rc.conf.d/rc.firewall -d /sbin/ipfw -f flush
Version minimale
$ cat -n /etc/rc.conf.d/rc.firewall 1 set -e 2 3 set_31() { 4 [ ${firewall_debug} = 'NO' -a $($fwcmd set 31 list | wc -l) -gt 1 ] && return 5 6 $add set 31 check-state 7 $add set 31 pass { dst-port 22 or src-port 22 } 8 } 9 10 . /etc/rc.subr 11 load_rc_config ipfw 12 13 set -u 14 15 [ $# -eq 1 ] && [ $1 = "-d" ] && firewall_debug=YES 16 17 fwcmd=/sbin/ipfw 18 case ${firewall_quiet:-} in 19 [Yy][Ee][Ss]) fwcmd="$fwcmd -q";; 20 esac 21 22 case ${firewall_debug:-NO} in 23 [Yy][Ee][Ss]) fwcmd="echo $fwcmd";; 24 *) firewall_debug='NO';; 25 esac 26 27 add="$fwcmd add" 28 29 set_31 30 $fwcmd -f flush 31 32 $add pass via lo0 33 $add pass out keep-state
Concernant l'utilisation du set 31:
$ man ipfw ... Set 31 is special in that it cannot be disabled, and rules in set 31 are not deleted by the ipfw flush command (but you can delete them with the ipfw delete set 31 command). Set 31 is also used for the default rule.
Il ne faut *SURTOUT PAS* que le trafic "ssh" fasse l'objet de règles dynamiques car elles sont supprimées lors d'un /sbin/ipfw -f flush.
- 4: si je ne suis pas en mode debug et que des règles sont déjà présentes dans le set 31 je ne fais rien
- 6: vérifications des règles dynamiques
- 7: j'autorise le ssh entrant/sortant sans utiliser de règles dynamiques
- 29: mise en place des règles du set 31
- 32: je laisse circuler le trafic de l'interface lo0
- 33: je laisse tout sortir en utilisant des règles dynamiques
Je peux maintenant me connecter sur la machine distante et y faire des sudo service ipfw restart ou des /sbin/ipfw -f flush sans perdre la connexion.
SSH restreint
Je ne suis pas du genre à laisser mon port ssh ouvert aux 4 vents mais je peux aussi avoir des gros doigts. Comme mon set 31 n'est défini qu'une et une seule fois, je peux faire un maximum de tests et je n'hésite pas à jouer avec le mode trace (/bin/sh /etc/rc.conf.d/rc.firewall -d).
3 set_31() { 4 [ ${firewall_debug} = 'NO' -a $($fwcmd set 31 list | wc -l) -gt 1 ] && return 5 6 $add set 31 check-state 7 if [ -z "${firewall_ssh6:-}" -a -z "${firewall_ssh4:-}" ]; then 8 $add set 31 pass { dst-port 22 or src-port 22 } 9 else 10 $add set 31 pass src-port 22 out 11 [ -n "${firewall_ssh6:-}" ] && $add set 31 pass dst-port 22 in src-ip6 ${firewall_ssh6} || : 12 [ -n "${firewall_ssh4:-}" ] && $add set 31 pass dst-port 22 in src-ip ${firewall_ssh4} || : 13 fi 14 } ... 40 $add pass { proto ipv6-icmp or proto icmp } 41 $add deny log in
- 7-8: si mes variables ne sont pas définies j'ouvre aux 4 vents
- 10: un paquet avec un port source à 22 en sortie ne peut venir que de ma machine donc je laisse passer
- 11-12: si une variable est définie alors laisser rentrer le trafic à destination du port 22 si l'adresse source correspond à la variable
- 40: je laisse passer l'icmp (qu'il soit ipv6 ou ipv4)
- 41: je loggue tout ce que je bloque en entrée
Au final
$ sudo ipfw -d -S show 00100 0 0 set 31 check-state :default 00200 3780 1509406 set 31 allow src-port 22 out 00300 3675 341057 set 31 allow dst-port 22 in src-ip6 2001:db8::/64 00400 0 0 set 31 allow dst-port 22 in src-ip 192.168.99.0/24 00500 1169 97093 set 0 allow out keep-state :default 00600 0 0 set 0 allow via lo0 00700 60 4332 set 0 allow { proto ipv6-icmp or proto icmp } 00800 2 656 set 0 deny log in 65535 0 0 set 31 deny ip from any to any
Il reste à voir les tables, le nat et 2/3 trucs auxquels je n'ai pas encore pensé.
Commentaires: https://github.com/bsdsx/blog_posts/issues/1