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 :

vpn_redondant_2_operateurs.png, août 2014

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 somme~~Le 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 :

bird_internal_tables.png, août 2014

É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.