Tags

bluetooth cu ipfw shell unbound

Powered by

blOg
maRkdown
awK
shEll

21/11/2020

[ ipfw shell ]

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:

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:

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


Lien vers ce billet

25/01/2020

[ ipfw shell ]

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:

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:

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

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

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


Lien vers ce billet

10/01/2020

[ ipfw shell ]

202001101600 ipfw shell

ipfw: modern FreeBSD, les tables

Les tables permettent de faire des listes de différents types:

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

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:

Contre:

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:

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:

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	}

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:

Commentaires: https://github.com/bsdsx/blog_posts/issues/2


Lien vers ce billet

10/01/2020

[ ipfw shell ]

202001101600 ipfw shell

ipfw: modern FreeBSD, les tables

Les tables permettent de faire des listes de différents types:

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

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:

Contre:

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:

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:

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	}

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:

Commentaires: https://github.com/bsdsx/blog_posts/issues/2


Lien vers ce billet

07/12/2019

[ ipfw shell ]

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:

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

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.

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

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


Lien vers ce billet