Passerelles VPN redondantes avec OpenBSD et BIRD
BIRD, c'est bon, mangez-en !
OpenBSD, c'est le papa de tous les IPSEC (enfin presque) et surtout OpenBSD, c'est truffé de petits outils pour rendre IPSEC plus résistant et résilient. On va voir aujourd'hui comment construire une passerelle VPN avec des morceaux de routage dynamique dedans.
L'hypothèse de départ
On a un site A avec deux passerelles VPN et un routeur OSPF et un site B avec seulement deux routeurs VPN. Pour cette exemple, on va supposer que chaque site à son propre opérateur (même si pour des raisons de simplicité, ce sera le même réseau pour la maquette) avec suffisamment d'adresses IPv4/v6 publiques (au moins 3 par opérateur).
Basiquement, ça va ressembler à ça :
Je ne détaillerai pas volontairement la configuration de pf
et pfsync
(c'est pas le but et vous êtes assez grands pour le faire tout seul, faut pas déconner non plus).
Configuration réseau
On va donc connement commencer par la configuration réseau. Sur le site A rien de particulier, si ce n'est la configuration de carp
pour la passerelle en elle-même sur la patte Internet :
inet 100.64.0.254 255.255.255.0 NONE advbase 1 advskew 0 carpdev vio0 vhid 20 pass labiteadudule
C'est en théorie inutile sur la patte LAN puisqu'OSPF qui va se charger d'annoncer les bonnes routes. En l'occurence, les passerelles étant actives/passives, il va falloir s'arranger pour que la passerelle active annonce la route et que la passerelle passive n'annonce rien du tout.
Côté site B, pas grand chose de plus que cette configuration là, même astuce pour le carp
:
inet 100.64.0.100 255.255.255.0 NONE advbase 1 advskew 0 carpdev vio0 vhid 30 pass lateteatoto
Sauf qu'il faut aussi prévoir un carp
côté LAN pour servir de passerelle aux machines distantes :
inet 192.168.1.254 255.255.255.0 NONE advbase 1 advskew 0 carpdev vio1 vhid 40 pass cocolasticot
inet6 alias fddc:a021:7c20:1::254 64
L'adresse IPv6 ici est totalement dispensable (rtadvd
sur OpenBSD permet d'annoncer des routes sur une interface carp
ou d'annoncer des priorités, donc aucun intérêt) mais ça peut simplifier le débogage.
Oh oui !! Mets-la moi dans le tunnel !!
Dernier petit point de configuration réseau : on va monter un tunnel gif
entre les deux paires de routeurs. Ça nous permettra de gérer le routage sans avoir à se prendre la tête avec les tables IPSEC d'OpenBSD, qui peuvent être particulièrement pénibles dans certains cas. Il est à noter également que les fameuses tables en question ne sont pas des tables de routage, parce qu'IPSEC n'est pas un protocole de routage. C'est duraille hein, mais c'est comme ça.
Du coup gif
et on se prend pas le chou.
Bref, la configuration est exactement symétrique entre le site A et le site B. On a donc en A :
tunnel 100.64.0.254 100.64.0.100
inet 169.254.0.0 255.255.255.254 169.254.0.1
inet6 fddc:a021:7c20:ffff::0 128 fddc:a021:7c20:ffff::1
Et en B :
tunnel 100.64.0.100 100.64.0.254
inet 169.254.0.1 255.255.255.254 169.254.0.0
inet6 fddc:a021:7c20:ffff::1 128 fddc:a021:7c20:ffff::0
Subtilité supplémentaire : on ouvre le tunnel entre les adresses publiques des interfaces carp
. Comme ça, le tunnel n'est pas vraiment ouvert sur le routeur passif (en théorie, en pratique, il peut arriver qu'il tente d'initier le tunnel, mais on a un moyen très simple de l'en empêcher).
Normalement avec cette configuration-là, on a déjà une communication possible au moins dans le tunnel. Il va simplement nous manquer les routes de part et d'autre du tunnel. Ainsi depuis bvpn-1 :
$ ping -c3 -I 169.254.0.1 169.254.0.0
PING 169.254.0.0 (169.254.0.0): 56 data bytes
64 bytes from 169.254.0.0: icmp_seq=0 ttl=255 time=0.646 ms
64 bytes from 169.254.0.0: icmp_seq=1 ttl=255 time=0.698 ms
64 bytes from 169.254.0.0: icmp_seq=2 ttl=255 time=0.635 ms
--- 169.254.0.0 ping statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss
round-trip min/avg/max/std-dev = 0.635/0.659/0.698/0.040 ms
$ ping6 -c3 -S fddc:a021:7c20:ffff::1 fddc:a021:7c20:ffff::
PING6(56=40+8+8 bytes) fddc:a021:7c20:ffff::1 --fddc:a021:7c20:ffff::
16 bytes from fddc:a021:7c20:ffff::, icmp_seq=0 hlim=64 time=0.685 ms
16 bytes from fddc:a021:7c20:ffff::, icmp_seq=1 hlim=64 time=0.832 ms
16 bytes from fddc:a021:7c20:ffff::, icmp_seq=2 hlim=64 time=0.724 ms
--- fddc:a021:7c20:ffff:: ping6 statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss
round-trip min/avg/max/std-dev = 0.685/0.747/0.832/0.062 ms
BIRD, le petit zozio sur la bran-cheuh
BIRD, c'est juste le meilleur routeur open source à ce jour. Si, si. Et c'est justement l'occasion d'en parler, parce que c'est vraiment un logiciel qui fait le café, la vaisselle, le ménage, qui te taille une petite pipe et qui se fait oublier après. La femme parfaite en sommeLe logiciel de routage préféré de Bibi ! Il fonctionne en IPv4 et en IPv6 avec une petite particularité : bird
gère l'IPv4 et bird6
l'IPv6.
BIRD sait parler OSPF, BGP, RIP et probablement deux ou trois autres trucs dont tout le monde se fout. Son principe de fonctionnement est relativement simple : on peut créer autant de routeur virtuel que l'on souhaite par protocole ; chaque de ces routeurs virtuels (protocols
dans BIRD) va échanger des routes avec une table interne (la BIRD Internal Routing Table). On peut décider ce qu'on injecte ou ce qu'on extrait de chaque protocole très facilement, via un (très) puissant système de filtre.
Si on prend un routeur OSPF standard, ça donnera à peu près ça :
Évidemment, chaque route dans la table interne va avoir tout un tas de propriétés associées (ici, je n'ai représenté que le RTS
Routing Table Source, mais il y en a plein d'autres).
On va donc configurer artr-1
pour qu'il annonce des routes statiques et ses propres interfaces connectées :
log syslog { warning, error };
router id 192.168.0.1;
## « direct » correspond aux routes connectées
protocol direct {
interface "vio*";
}
## « kernel » correspond aux routes du
## noyau (routes statiques ajoutées à la main par exemple)
## c'est aussi grâce à lui qu'on va exporter la table BIRD
## vers le système
protocol kernel {
persist; # les routes sont persistentes même si BIRD plante
scan time 20;
export all;
}
# pseudo-protocole permettant de surveiller les interfaces
protocol device {
scan time 10;
}
## pour les routes statiques gérées par BIRD
protocol static {
route 10.0.0.0/8 via "lo0";
route 172.16.0.0/12 via "lo0";
route 192.168.0.0/16 via "lo0";
}
## notre fameux routeur OSPF
protocol ospf {
import all;
export all;
area 0.0.0.0 {
interface "vio0" {
};
};
}
Oui, oui, c'est tout :). Le protocole static
va permettre de créer des routes statiques un peu bidon histoire qu'on ait quelque chose à annoncer en OSPF (par défaut, il est en import all
donc toutes les routes statiques seront envoyées dans BIRD). Le protocole kernel
permet d'écrire les routes de la BIRD Internal Routing Table vers le système pour « réellement » router.
Pour la partie IPv6, c'est presque tout pareil : on prend le fichier /etc/bird.conf
et le copie en /etc/bird6.conf
en changeant simplement les routes statiques. C'est l'utilitaire birdc
qui permet de communiquer directement avec BIRD, démonstration :
$ birdc
BIRD 1.4.0 ready.
birdshow ospf neighbors
ospf1:
Router ID Pri State DTime Interface Router IP
birdshow route
10.0.0.0/8 dev lo0 [static1 10:15:23] * (200)
192.168.0.0/24 dev vio0 [direct1 10:15:23] * (240)
dev vio0 [ospf1 10:15:24] I (150/10) [192.168.0.1]
192.168.0.0/16 dev lo0 [static1 10:15:23] * (200)
172.16.0.0/12 dev lo0 [static1 10:15:23] * (200)
Et toutes les routes se retrouvent bien dans le noyau :
$ route -n show -inet
Routing tables
Internet:
Destination Gateway Flags Refs Use Mtu Prio Iface
10/8 127.0.0.1 U1 0 0 33192 56 lo0
127/8 127.0.0.1 UGRS 0 0 33192 8 lo0
127.0.0.1 127.0.0.1 UH 1 0 33192 4 lo0
172.16/12 127.0.0.1 U1 0 0 33192 56 lo0
192.168.0/24 link#1 UC 1 0 - 4 vio0
192.168/16 127.0.0.1 U1 0 0 33192 56 lo0
192.168.0.1 52:54:00:1f:76:10 UHLc 0 4 - 4 lo0
224/4 127.0.0.1 URS 0 0 33192 8 lo0
Configuration OSPF des VPN du site A
Pour les VPN du site A, nous allons avoir un petit souci. Si on annonce toutes les routes connectées (incluant donc le tunnel gif
), ça ne va pas beaucoup nous aider à router. Il va donc falloir annoncer cela sous forme de route statique. Mais comme on ne peut pas annoncer la même route statique des deux côtés, il va falloir procéder autrement.
Côté site B tout d'abord, on va ajouter des routes statiques à la montée des interfaces gif
. Pas très compliqué, il suffit de rajouter cela à la fin du fichier /etc/hostname.gif0
:
!route -n add -inet 10/8 169.254.0.0
!route -n add -inet 172.16/12 169.254.0.0
!route -n add -inet 192.168/16 169.254.0.0
!route -n add -inet6 fc00::/7 fddc:a021:7c20:ffff::0
ême principe côté site A :
!route -n add -inet 192.168.1/24 169.254.0.1
!route -n add -inet6 fddc:a021:7c20:1::/64 fddc:a021:7c20:ffff::1
Ainsi au démarrage de l'interface, les routes sont montées automatiquement dans le noyau et il suffit de demander à BIRD de les apprendre. Pour empêcher les routes d'être annoncées sur le routeur de secours côté site A, on va se servir de ifstated
pour faire monter/descendre les interfaces en fonction du maître. Ci-dessous /etc/ifstated.conf
:
init-state auto ## état d'origine au démarrage de ifstated
## variable prenant true ou false en fonction de l'état de carp0
fw_carp_up = "carp0.link.up"
fw_carp_init = "carp0.link.unknown"
state auto {
if ($fw_carp_init)
run "sleep 10"
if ($fw_carp_up)
set-state fw_master
if (! $fw_carp_up)
set-state fw_slave
}
state fw_master { # si on devient master CARP
init {
run "ifconfig gif0 up"
}
if($fw_carp_init)
run "sleep 2"
if(! $fw_carp_up)
set-state fw_slave
}
state fw_slave { # si on devient slave CARP
init {
run "ifconfig gif0 down"
}
if($fw_carp_init)
run "sleep 2"
if($fw_carp_up)
set-state fw_master
}
Avec cette astuce, l'esclave ne peut jamais transmettre la route (puisque l'interface sous-jacente est down systématiquement). Passons maintenant à la configuration de BIRD pour les passerelles VPN en question. On va être obligé dans cet exemple d'apprendre les routes « aliens » venant du noyau (puisqu'on ajoute/supprime des routes à la volée via ifstated
). Il va donc falloir filtrer ces routes sinon, ça va tourner au grand nawak très rapidement.
log syslog { warning, error };
router id 192.168.0.251;
protocol kernel {
learn; # on force l'apprentissage des routes, import ne suffit pas
persist;
scan time 20;
import filter { ## un petit filtre des familles
if dest = RTD_UNREACHABLE then reject; # routes non-joignables
# c'est surtout utile en IPv6 où il y a des routes bannies dans OpenBSD par défaut
if net ~ [ 0.0.0.0/0 ] then reject; # passerelle par défaut
accept;
};
export all;
}
protocol device {
scan time 10;
}
protocol ospf {
import all;
export all;
area 0.0.0.0 {
interface "vio1" {
};
};
}
Et évidemment pareil en IPv6, mais avec une route par défaut qui a une tronche un peu différente.
Avec tout ce bazar, tout fonctionne très bien à présent, le routage est parfaitement opérationnel. Depuis artr-1 :
$ ping -c3 192.168.1.1
PING 192.168.1.1 (192.168.1.1): 56 data bytes
64 bytes from 192.168.1.1: icmp_seq=0 ttl=253 time=1.631 ms
64 bytes from 192.168.1.1: icmp_seq=1 ttl=253 time=1.782 ms
64 bytes from 192.168.1.1: icmp_seq=2 ttl=253 time=1.598 ms
--- 192.168.1.1 ping statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss
round-trip min/avg/max/std-dev = 1.598/1.670/1.782/0.086 ms
$ ping6 -c3 fddc:a021:7c20:1::1
PING6(56=40+8+8 bytes) fddc:a021:7c20::1 --fddc:a021:7c20:1::1
16 bytes from fddc:a021:7c20:1::1, icmp_seq=0 hlim=62 time=1.654 ms
16 bytes from fddc:a021:7c20:1::1, icmp_seq=1 hlim=62 time=2.075 ms
16 bytes from fddc:a021:7c20:1::1, icmp_seq=2 hlim=62 time=1.825 ms
--- fddc:a021:7c20:1::1 ping6 statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss
round-trip min/avg/max/std-dev = 1.654/1.851/2.075/0.173 ms
On peut même tester que les routes montent/descendent correctement sur avpn-1/2 en jouant un peu avec carpdemote
.
Bon ben du coup, on chiffre ou bien
Ça vient jeune puceau, ça vient. Pour faire de la redondance dans l'IPSEC, il va nous falloir trois éléments :
ipsec
activé ;isakmpd
pour génerer les sessions IPSEC ;sasyncd
pour synchroniser les sessions en question entre chaque paire de routeurs.
Pour les deux premier, c'est extrêmement simple, il suffit d'ajouter ça dans /etc/rc.conf.local
:
ipsec=YES
isakmpd_flags="-K -S"
Au passage, on peut tout de suite mettre :
sasyncd_flags=""
Et configurer sasyncd
:
## donne le port d'écoute et l'adresse du copain
listen on 192.168.0.251
peer 192.168.0.252
# l'interface carp à surveiller pour savoir si on est maître ou esclave
interface carp0
# une clé partagée
sharedkey 0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
Ça, c'est la configuration pour ''avpn-1''. Il faut bien évidemment faire un symétrique pour ''avpn-2'' et le même genre de chose (mais avec une autre clé) pour bvpn-1/2.
Et finalement, il va falloir configurer la partie IPSEC en elle-même. Là, ça se passe dans /etc/ipsec.conf
avec une configuration relativement simple (ici pour les deux routeurs du site A, la configuration est symétrique pour B) :
## macros de définition des deux sites
avpn="100.64.0.254"
bvpn="100.64.0.100"
## en une seule ligne, le protocole à transporter dans le tunnel et les différents chiffrements pour les phases 1 et 2 d'IPSEC
ike esp transport proto ipencap from $avpn to $bvpn local $avpn peer $bvpn main auth hmac-sha1 enc aes group modp1536 quick auth hmac-sha1 enc aes group modp1536 psk "pipopipo"
ike esp transport proto ipv6 from $avpn to $bvpn local $avpn peer $bvpn main auth hmac-sha1 enc aes group modp1536 quick auth hmac-sha1 enc aes group modp1536 psk "pipopipo"
L'astuce consiste ici à encapsuler dans IPSEC les protocoles IP ipencap
et ipv6
qui correspondent respectivement à IPv4 dans IPv4 et IPv6 dans IPv4. L'ensemble de ce qui passe dans le tunnel gif
sera donc chiffré entre les sites A et B.
Une fois que c'est fait, il n'y a plus qu'à redémarrer le tout pour que tout soit pris en compte.
TADAAAAA !!
Les flux IPSEC devraient normalement s'établir relativement vite entre les deux routeurs maîtres des deux côtés et les flux et les sessions vont assez vite se répliquer vers les routeurs esclaves des deux côtés :
$ ipsecctl -sf
flow esp in proto ipv6 from 100.64.0.100 to 100.64.0.254 peer 100.64.0.100 srcid 100.64.0.254/32 dstid 100.64.0.100/32 type use
flow esp out proto ipv6 from 100.64.0.254 to 100.64.0.100 peer 100.64.0.100 srcid 100.64.0.254/32 dstid 100.64.0.100/32 type require
flow esp in proto ipencap from 100.64.0.100 to 100.64.0.254 peer 100.64.0.100 srcid 100.64.0.254/32 dstid 100.64.0.100/32 type use
flow esp out proto ipencap from 100.64.0.254 to 100.64.0.100 peer 100.64.0.100 srcid 100.64.0.254/32 dstid 100.64.0.100/32 type require
Note personnelle : pour une raison que j'ignore, le nombre de sessions actives quand on est en IKE actif/actif est nettemment plus important qu'en IKE actif/passif (avec un routeur qui initie la connexion donc). Si quelqu'un a une explication, je suis preneur.
On peut maintenant rebooter à loisir l'un ou l'autre des routeurs de n'importe quel côté et conserver la connexion et le chiffrement sans aucun souci.
Pour la prochaine fois, on va faire la version plus sophistiquée, en supposant qu'un des deux côtés à deux opérateurs au lieu d'un.