SwordArMor

Avoir une sorte de flowspec sous linux avec ipset

Internet étant truffé de machines plus ou moins vérolées qui scannent le monde entier, j’ai eu envie de les bloquer à l’échelle complète de mon réseau ; un genre de fail2ban2bgp.
Avec netfilter il n’existe pas de mécanisme pour avoir des règles dynamiques, et donc pas de moyen d’avoir facilement des règles flowspec dans son firewall. Par contre, il est possible d’utiliser un ipset comme source dans une règle. Et avec exabgp il est possible d’utiliser un script shell pour transformer une annonce bgp en ipset. J’ai donc utilisé ceci pour implémenter une sorte de flowspec sur des routeurs soft linux.

Voici le schéma global de la solution que j’ai mise en place :

  • Envoyer les logs à une machine (avec rsyslog)

  • Les faire traiter par fail2ban

  • Lui faire créer des routes locales sur la machine (avec une banaction)

  • Annoncer ces routes en BGP (avec bird)

  • Envoyer ces routes à tous mes routeurs (avec des RRs)

  • Transformer ces routes en règles netfilter (avec exabgp, ipset et un script shell)

À noter : pour me besoin je n’ai besoin que de bloquer des IPs sources, je n’ai donc pas du tout regardé pour aller plus finement dans le filtrage (au port, à la taille de paquet, etc.). Je n’ai pas non plus implémenté la suppression des routes en cas de withdrawn bgp, parce que j’ai envie que les règles soient persistantes. J’ai utilisé ipset pour pouvoir plus facilement le mettre en place si besoin, mais on pourrait simplifier en écrivant directement des règles iptables. Je me suis également contenté d’IPv4, parce que je n’ai vu personne pourrir mes logs en IPv6 pour le moment, mais le code est très facilement adaptable.

Je ne vais pas aborder les configurations rsyslog et fail2ban ici, ce n’est pas mon but.

La récupération des routes générées par fail2ban se passent très classiquement avec de la conf bird relativement simple. J’ai un filtre d’import sur le protocol kernel qui ajoute une communauté sur les routes unreachables, et un filtre d’export bgp qui utilise cette communauté. Cette machine là peere avec mes RRs pour que les routes soient redistribuées dans mon réseau.

protocol kernel kernel_grt_ipv4 {
	learn;
	persist;

	ipv4 {
		import filter {
			if (dest = RTD_UNREACHABLE) then {
				bgp_large_community.add((208627,666,666));
				accept;
			}
			else {
				reject;
			}
		};
		export none;
	};
}

protocol bgp ibgp_core02_ipv4 from t_ibgp_ipv4 {
	neighbor 10.0.4.4 as 208627;

	ipv4 {
		import none;
		export filter {
			if (bgp_large_community ~ [(208627,666,666)]) then {
				accept;
			}
			else {
				reject;
			}
		};
	};
}

Ensuite, sur les routeurs j’envoie les routes avec celle communauté sur un exabgp en localhost. La version que j’utilise (4.2.17) a un bug, toutes les commandes de cli finissent en timeout… Donc si vous comptez utiliser un truc du genre en prod, tentez les versions plus anciennes ou faites chier les devs pour qu’ils pissent du code utilisable. Quand j’ai remonté le souci, on m’a dit d’utiliser master, ce que je n’ai pas envie de faire, ça me semble plus sain de vivre avec ce bug pour le moment.

# /etc/bird.conf
protocol bgp bgp_drop_v4 from iBGP_IPv4 {
	description "locahost exabgp";
	neighbor 127.0.0.1 port 1179 as myasn;
	local 127.0.0.1;
	rr client;

	ipv4 {
		import none;
		export filter {
			if ((208627,666,666) ~ bgp_large_community) then {
				accept;
			}
			else {
				reject;
			}
		};
	};
}

# /etc/exabgp/exabgp.env
[exabgp.tcp]
bind=127.0.0.1
port=1179

# /etc/exabgp/exabgp.conf
process acl {
	run /usr/local/libexec/acl.sh;
	encoder text;
}

template {
	neighbor fw_v4 {
		family {
			ipv4 unicast;
		}
		api speaking {
			processes [ acl ];
			receive {
				parsed;
				update;
			}
		}
	}
}

neighbor 127.0.0.1 {
	inherit fw_v4;
	router-id 127.0.0.1;
	local-address 127.0.0.1;
	local-as 208627;
	peer-as 208627;
}

# /usr/local/libexec/acl.sh
#!/bin/sh

exec 2>&1> /tmp/exa

while read stdin; do
	net="$(echo $stdin | awk '/announced/ { print $6 }')"
	if [ -n "${net}" ]; then
		neigh="$(echo $stdin | awk '/announced/ { print $2 }')"
		if [ "${neigh}" = '127.0.0.1' ]; then
			sudo ipset add firewall_v4 $net
		elif [ "${neigh}" = '::1' ]; then
			sudo ipset add firewall_v6 $net
		fi
	fi
done < /dev/stdin

J’ai hardcodé le nom de l’ipset utilisé parce que ce code n’est pas voué à être générique, mais vous pouvez lancer le script avec des arguements depuis exabgp. J’utilise sudo parce l’utilisateur exabgp n’a pas le droit de lancer ipset par défaut (ce qui se comprend assez bien).
Le code part du principe que l’ipset existe déjà (ipset create firewall_v4 hash:net family inet counters).

Ensuite, dans iptables j’utilise l’ipset ainsi peuplé pour bloquer les IPs qui sont dedans

iptables -A INPUT -m set --match-set firewall_v4 src -j DROP
iptables -A FORWARD -m set --match-set firewall_v4 src -j DROP
ip6tables -A INPUT -m set --match-set firewall_v6 src -j DROP
ip6tables -A FORWARD -m set --match-set firewall_v6 src -j DROP

Ce qui apparaît sous la forme DROP all -- * * 0.0.0.0/0 0.0.0.0/0 match-set firewall_v4 src dans iptables -L -n -v

« Et voilà », je bloque les IPs sur tout mon réseau grâce à ça, et je ne perds pas toutes les règles en cas de flap bgp.