19 mars 2024

Haute Disponibilité de routeurs sous Linux avec Keepalived

Dans cet article, nous allons mettre en place le premier service fourni dans notre infrastructure KVM : le routage vers et depuis l’extérieur.

Bah oui, avoir des VMs, c’est cool, qu’elles puissent à minima se mettre à jour et pourquoi pas, fournir d’autres services externe, c’est encore mieux !

Cet article s’intègre dans la série New Box 2022. Vous y trouverez le pourquoi du comment, les prérequis, paquets à installer, etc…

 

I – Explication

Nous allons donc mettre en place non pas un mais deux routeurs !
En effet, dans mes dernières infra, ce restait un sérieux SPOF (Single Point Of Failure), où en bon français, « Si ça pète, c’est la merde »…

Pour rappel, voila ce que nous allons mettre en place, nous concentrant dans ce tutoriel sur Extrout1 et Extrout2.

Par défaut, comme toutes mes VMS, ces deux routeurs ont un lien dans le VLAN 99 (lien VMs <-> Hyperviseur) et dans le VLAN 250 ( lien administration inter-vm, utilisé pour le monitoring, etc…). Histoire de ne pas complexifier inutilement, ces deux VLANs ne sont pas représentés sur le schéma.

Il leurs faudra également un lien pour communiquer avec l’extérieur via le bridge br0 et une lien dans le VLAN 10 pour servir de Gateway à mes machines qui causent en direct avec le net (FrontWeb, ….)

Via un mécanisme de Haute Dispo, le routeur Master aura deux IPs flottantes, une IP 10.10.1.254 servant de Gateway pour mes VMs et l’IP externe.

Puis ensuite, du NAT, du FW, bref, du classique…

Dans mon contexte, je rappelle que je suis sur une Dédibox, chez Online et que j’ai au préalable commandé une IP failover et ai généré une MAC correspondante.

Dans un contexte @home, peu d’importance, mais chez Online, attention, sinon coupure !

 

II – Mise en place

A – Créations des VMs

On va créer nos deux routeurs à l’aide des scripts présentés ici : Tuto Virtualisation avec KVM, partie VIII : Scripts d’automatisation

Au niveau ressources systèmes,  dans le script create.sh, je les configure ainsi :

# Options
size_swap=1
size_var=3
size=3
ram=256
cpu=1

Tout comme Maxwell, pas la peine d’en rajouter.

On lance la création des VMs :

hyperv# ./create_vm.sh -n extrout1 -i 1 -s 1
hyperv# ./create_vm.sh -n extrout2 -i 2 -s 0

Extrout1 sur le VG Master, id 1 et Extrout2, sur le VG Slave, id 2.

Pour vérifier :

hyperv# virsh list --all

doit répondre :

Id   Name       State
---------------------------
-    extrout1   shut off
-    extrout2   shut off

 

B – Ajout des nouvelles interfaces

Pour le moment, nos machines sont uniquement branchées sur les VLANs 99 et 250 avec comme adresses :

  • extrout1 : 10.99.1.1 et 10.250.1.1
  • extrout2 : 10.99.1.2 et 10.250.1.2

C’est bien mais pas top… En effet, pour communiquer avec le réseau externe et avec nos futurs VM, il va en falloir plus !

Et comme je l’ai déjà mentionné, il faut deux nouvelles interfaces : une pour le lien externe via br0, l’autre pour le VLAN 10.

On va ajouter les interfaces réseaux sur extrout1 :

# virsh attach-interface --domain extrout1 --type network --source intern-vlans --target extrout1.1 --model virtio --config
# virsh attach-interface --domain extrout1 --type network --source ovs-front --target extrout1.2 --model virtio --config --mac 52:54:00:ff:ff:ff

La première, extrout1.1, sera utilisée pour communiquer sur le VLAN 10.
La seconde, extrout1.2, sera utilisée pour communiquer sur le réseau internet. Bien évidement dans le contexte « Chez Scaleway/Online », on fait attention à la MAC, pour ma part, c’est celle virtuelle associée à mon IP Fail Over. Pour info le préfixe 52:54:00 est dédié à KVM.

La même chose pour extrout2 :

# virsh attach-interface --domain extrout2 --type network --source intern-vlans --target extrout2.1 --model virtio --config
# virsh attach-interface --domain extrout2 --type network --source ovs-front --target extrout2.2 --model virtio --config --mac 52:54:00:ff:ff:ff

La aussi, en prenant soin de mettre la MAC correcte, si besoin.

Et pour vérifier tout ceci ? Et bien :

hyperv# virsh domiflist extrout1

Qui doit nous donner un joli :

Interface    Type      Source         Model    MAC
-------------------------------------------------------------------
extrout1.0   network   intern-vlans   virtio   52:54:00:aa:bb:cc
extrout1.1   network   intern-vlans   virtio   52:54:00:dd:ee:ff
extrout1.2   network   ovs-front      virtio   52:54:00:ff:ff:ff

 

C – Correction de la configuration

Un petit défaut, c’est qu’ajouter une interface de la sorte (en CLI) ne permet pas de spécifier le vlan associé. On doit donc éditer la configuration pour y remédier.

J’aurais pu vous montrer de suite la technique d’ajout d’interface par injection de fichier xml, mais on verra cette méthode par la suite.

Pour éditer la configuration d’une VM :

hyperv# virsh edit extrout1

Vous vous retrouvez alors avec un fichier xml décrivant la configuration de votre VM. Si vous descendez un peu, vous trouverez la partie dédiée au réseau :

...
    <interface type='network'>
      <mac address='52:54:00:aa:bb:cc'/>
      <source network='intern-vlans' portgroup='vlansadmin'/>
      <target dev='extrout1.0'/>
      <model type='virtio'/>
      <driver name='vhost'/>
      <address type='pci' domain='0x0000' bus='0x01' slot='0x00' function='0x0'/>
    </interface>
    <interface type='network'>
      <mac address='52:54:00:dd:ee:ff'/>
      <source network='intern-vlans' portgroup='vlan10'/>
      <target dev='extrout1.1'/>
      <model type='virtio'/>
      <driver name='vhost'/>
      <address type='pci' domain='0x0000' bus='0x09' slot='0x00' function='0x0'/>
    </interface>
    <interface type='network'>
      <mac address='52:54:00:ff:ff:ff'/>
      <source network='ovs-front'/>
      <target dev='extrout1.2'/>
      <model type='virtio'/>
      <driver name='vhost'/>
      <address type='pci' domain='0x0000' bus='0x0a' slot='0x00' function='0x0'/>
    </interface>
...

Vous ajoutez les éléments en gras.

En quittant, on sauvegarde et on doit avoir :

Domain 'extrout1' XML configuration edited.

et pour extrout2, d’une manière identique :

hyperv# virsh edit extrout1

Pour éditer de la sorte :

...
    <interface type='network'>
      <mac address='52:54:00:ab:ab:ab'/>
      <source network='intern-vlans' portgroup='vlansadmin'/>
      <target dev='extrout2.0'/>
      <model type='virtio'/>
      <driver name='vhost'/>
      <address type='pci' domain='0x0000' bus='0x01' slot='0x00' function='0x0'/>
    </interface>
    <interface type='network'>
      <mac address='52:54:00:cd:cd:cd'/>
      <source network='intern-vlans' portgroup='vlan10'/>
      <target dev='extrout2.1'/>
      <model type='virtio'/>
      <driver name='vhost'/>
      <address type='pci' domain='0x0000' bus='0x09' slot='0x00' function='0x0'/>
    </interface>
    <interface type='network'>
      <mac address='52:54:00:ff:ff:ff'/>
      <source network='ovs-front'/>
      <target dev='extrout2.2'/>
      <model type='virtio'/>
      <driver name='vhost'/>
      <address type='pci' domain='0x0000' bus='0x0a' slot='0x00' function='0x0'/>
    </interface>
...

Et on sait qu’on a bien bossé en quittant avec :

Domain 'extrout2' XML configuration edited.

 

D – Configuration réseau des routeurs

Avant de lancer les VMs, on va éditer leur configuration réseau.

On va tout d’abord créer un répertoire pour le montage des partitions :

hyperv# mkdir /root/mountlvm

On monte le volume correspondant au / (racine) de la vm extrout1

hyperv# mount /dev/mapper/vg1-extrout1p1 /root/mountlvm/

Note: si le volume à monter est par exemple extrout, sans le 1, la partition ne sera pas extrout1p1 comme dans l’exemple présent, mais simplement extrout1.

On édite le fichier /root/mountlvm/etc/network/interfaces en ajoutant à la  fin :

auto lo
iface lo inet loopback

auto eth0.99
iface eth0.99 inet static
 address 10.99.1.1
 netmask 255.255.255.0

auto eth0.250
iface eth0.250 inet static
 address 10.250.1.1
 netmask 255.255.255.0

auto eth1
iface eth1 inet static
 address 10.10.1.1
 netmask 255.255.255.0

On démonte :

hyperv# umount /root/mountlvm

Et on refait une manipulation similaire pour le second routeur :

hyperv# mount /dev/mapper/vg0-extrout2p1 /root/mountlvm/

On édite le fichier /root/mountlvm/etc/network/interfaces en ajoutant à la  fin :

auto lo
iface lo inet loopback

auto eth0.99
iface eth0.99 inet static
 address 10.99.1.2
 netmask 255.255.255.0

auto eth0.250
iface eth0.250 inet static
 address 10.250.1.2
 netmask 255.255.255.0

auto eth1
iface eth1 inet static
 address 10.10.1.2
 netmask 255.255.255.0

Et on démonte :

hyperv# umount /root/mountlvm

 

E – Mise en route

On démarre nos machines

hyperv# virsh start extrout1
hyperv# virsh start extrout2

Pour mémoire, pour lancer en mode console :

hyperv# virsh start extrout1 --console

Et pour accéder à la console d’une VM :

hyperv# virsh console extrout1

 

Mais dans notre cas, on passera par SSH.

On vérifie tout d’abord, dans l’hyperviseur, que les ports sont branchés comme il faut :

hyperv# ovs-vsctl show
4ed3127c-77c6-4843-87eb-f1e753e8f8e9
  Bridge brint
    Port brint
      tag: 99
      Interface brint
      type: internal
    Port "extrout1.1"
      tag: 10
      Interface "extrout1.1"
    Port "extrout2.1"
      tag: 10
      Interface "extrout2.1"
    Port "extrout1.0"
      trunks: [99, 250]
      Interface "extrout1.0"
    Port "extrout2.0"
      trunks: [99, 250]
      Interface "extrout2.0"
  Bridge "br0"
    Port "br0"
      Interface "br0"
      type: internal
    Port "extrout2.2"
      Interface "extrout2.2"
    Port "eno1"
      Interface "eno1"
    Port "extrout1.2"
      Interface "extrout1.2"

Et si tout est bien fait, on doit pouvoir y accéder par SSH :

hyperv# ssh user@10.99.1.1

et

hyperv# ssh user@10.99.1.2

ou bien :

hyperv# ssh user@extrout1

 

Jusque la, tout est bon ? Parfait alors, continuons !

 

III – Configuration réseau

On se connecte donc au routeur 1 et on va va lui ajouter de quoi causer à travers l’interface eth2, qui je rappelle est branchée sur br0, avec la mac virtuelle fournie par Online.

Si j’étais chez moi, sur mon réseau 192.168.1.0/24, mon hyperviseur pourrait être en .250 et mon IP FO pour mes routeurs en .100 par exemple

A – Extrout1

Au préalable, il faut passer root :

extrout1$ su - root

Puis :

extrout1# ip addr add 123.123.123.123 dev eth2
extrout1# ip link set eth2 up
extrout1# ip route add 62.210.0.1 dev eth2
extrout1# ip route add 0.0.0.0/0 via 62.210.0.1 dev eth2
extrout1# echo 1 > /proc/sys/net/ipv4/ip_forward

Vous remplacez bien évidement 123.123.123.132 et 62.210.0.1 (la GW d’Online).
Dans l’ordre, on configure l’IP, on monte l’interface, on ajoute les routes qui vont bien (comment adresser la GW et ensuite comment l’utiliser) et on termine en activant le forward.

On test en effectuant un ping depuis une machine autre :

machineexterne# ping IP.FO

et depuis le routeur 1, un ping vers l’extérieur doit fonctionner aussi

extrout1# ping 8.8.4.4

 

B – Extrout2

Sur le routeur 2 :

On passe root :

extrout2$ su - root

Puis :

extrout2# ip route add 0.0.0.0/0 via 10.10.1.1 dev eth1 src 10.10.1.2

On ajoute simplement la route pour sortir via le routeur 1 sur le réseau 10.10.0.0/8.

 

C – Rapide FW avec NFTables

Mais il manque encore le NAT sur le routeur 1 pour que le routeur 2 puisse « sortir » sur le net. On va faire une rapide configuration de nftables.

NB : NAT, ça ne vous parle pas ? Vous voulez creuser le sujet Nftables, le remplaçant d’Iptables ? Alors, rendez vous ici : Nftables, la série de tutos

Sur les deux routeurs, nous allons créer :

Un premier fichier /srv/firewallup.sh

#!/usr/sbin/nft -f
flush ruleset
define external_ip = 123.123.123.123
define if_v10 = "eth1"

table ip filter {
  chain input {
    type filter hook input priority 0; policy accept;
  }
  chain output {
    type filter hook output priority 0; policy accept;
  }
  chain forward {
  type filter hook forward priority 0; policy accept;
  }
}
table ip nat {
  chain prerout {
    type nat hook prerouting priority -100; policy accept;
  }
  chain postrout {
    type nat hook postrouting priority 100; policy accept;
    iifname $if_v10 snat $external_ip
  }
}

Vous remplacez bien évidement l’IP 123.123.123.123.

Le point important dans cette configuration est la règle de POSTROUTING qui effectue un SNAT sur les paquets sortants (remplacement de l’adresse source en 10. par l’IP externe du routeur).

Puis un fichier /srv/firewalldown.sh, toujours sur les deux routeurs :

#!/usr/sbin/nft -f
flush ruleset
define if_v10 = "eth1"
table ip filter {
  chain input {
    type filter hook input priority 0; policy accept;
  }
  chain output {
    type filter hook output priority 0; policy accept;
  }
}

On chmod :

# cd /srv
# chmod +x firewall*

Sur le routeur1 on le lance :

# ./firewallup.sh

Et le routeur2 peut communiquer avec les réseaux externes.

Concernant le FW, nous reviendrons dessus dans un article à suivre.

 

IV – Haute Disponibilité avec Keepalived

Maintenant que nos deux routeurs sont prêts, nous allons passer à la mise en œuvre de la haute disponibilité du service.

Pour se faire, j’utilise Keepalived (https://www.keepalived.org/). Pour résumer brièvement, Keepalived permet d’assurer une Haute Disponibilité par la fourniture d’IPs « flottantes ». Un serveur est déclaré Maitre (en fonction de son poids, de l’état de ses pairs, etc…). Les IPs virtuelles (abrégés IPV) sont alors affectées à cet hôte. En cas de bascule, elles seront déplacées sur le nouveau Master. Je parle dans mon cas au futur car il y en aura plusieurs, mais bien évidement, qui peut le plus, peut le moins….

A – Installation et configuration

On l’installe sur les deux routeurs :

extroutX# apt install keepalived

Puis, sur le routeur 1,dans un fichier /etc/keepalived/keepalived.conf (le fichier n’existe pas, il faut le créer) :

global_defs {
  shutdown_script /srv/keepdown.sh
}
vrrp_instance ROUTEUR_1 {
  state MASTER
  preempt
  interface eth1
  virtual_router_id 1
  priority 200
  advert_int 1
  authentication {
    auth_type PASS
    auth_pass password
  }
  notify /srv/keep.sh
  unicast_src_ip 10.10.1.1
  unicast_peer {
    10.10.1.2
  }
  virtual_ipaddress {
    10.10.1.254
  }
}

Sur le routeur 2, fichier /etc/keepalived/keepalived.conf :

global_defs {
  shutdown_script /srv/keepdown.sh
}
vrrp_instance ROUTEUR_1 {
  state BACKUP
  interface eth1
  virtual_router_id 1
  priority 50
  advert_int 1
  authentication {
    auth_type PASS
    auth_pass password
  }
  notify /srv/keep.sh
  unicast_src_ip 10.10.1.2
  unicast_peer {
    10.10.1.1
  }
  virtual_ipaddress {
    10.10.1.254
  }
}

 

B – Le script de changement d’état

Maintenant, passons au script qui sera appelé à chaque changement d’état.

Sur les deux routeurs, créez un fichier /srv/keep.sh :

#!/bin/bash

TYPE=$1
NAME=$2
STATE=$3

case $STATE in
  "MASTER") /srv/keepup.sh
            exit 0
            ;;
  "BACKUP") /srv/keepdown.sh
            exit 0
            ;;
  "FAULT")  /srv/keepdown.sh
            exit 0
            ;;
  *)        echo "unknown state"
            exit 1
            ;;
esac

 

C – Script UP

Quand un routeur prend l’état MASTER, il faut donc qu’il se connecte via le br0 à Internet.

On va faire ainsi, avec, sur les deux routeurs, un fichier /srv/keepup.sh :

#!/bin/bash
ip addr add 123.123.123.123 dev eth2
ip link set eth2 up
ip route add IP.GATEWAY dev eth2
ip route del default
ip route add 0.0.0.0/0 via IP.GATEWAY dev eth2
echo 1 > /proc/sys/net/ipv4/ip_forward
/srv/firewallup.sh
sleep 1
ping -c 1 IP.GATEWAY

Vous remplacez bien sur 123.123.123.123 par l’IP réelle et IP.GATEWAY par l’IP de votre GW.

Le script configure l’IP, monte l’interface et refait une nouvelle route pour 0.0.0.0/0. Il termine en activant le forward et en chargeant une configuration de firewall.

Ha ! Et pourquoi un ping sur la GW pour terminer ?

Lors de mes tests, il apparaissait que le temps de bascule était… aléatoire…  La méthode était simple, une machine externe, un ping en boucle et je regardais combien j’en perdais. Au mieux, un, au pire… plein…. trop !

La raison se trouve dans les tables CAM d’Openvswitch qui ne se mettent à jour que si la VM initie une nouvelle connexion vers l’extérieur…

Et la méthode à Dudul, c’est un p’tit ping ! Sans ce dernier, on est dépendant du temps que va mettre un hypothétique service à faire une requête vers l’externe…

 

D – Script DOWN

Script appelé quand un routeur prend l’état SLAVE.

Fichier /srv/keepdown.sh, différent sur chaque routeur.

Sur le routeur 1 :

#!/bin/bash
ip link set eth2 down
ip addr del 123.123.123.123/32 dev eth2
ip route add 0.0.0.0/0 via 10.10.1.2 dev eth1 src 10.10.1.1
/srv/firewalldown.sh

Sur le routeur 2 :

#!/bin/bash
ip link set eth2 down
ip addr del 123.123.123.123/32 dev eth2
ip route add 0.0.0.0/0 via 10.10.1.1 dev eth1 src 10.10.1.2
/srv/firewalldown.sh

Dans les deux cas, vous remplacez 123.123.123.123 par l’IP réelle.

On termine en chmodant (oui, oui, verbe du premier groupe, vous ne connaissiez pas ? Je chmode, tu chmodes…)

# cd /srv
# chmod +x keep*

 

V – Tests

Maintenant, on va passer à la phase de validation en testant tout ceci.

On va tout d’abord arrêter Keepalived sur les deux routeurs :

extroutX# service keepalived stop

Sur les deux routeurs, on peut zieuter le log en parallèle pour voir ce qui se passe :

extroutX# tail /var/log/syslog -f

Puis on le relance sur extrout1 :

extrout1# service keepalived start

Dans le log, on doit voir ceci :

Apr 14 10:57:16 extrout1 Keepalived_vrrp[3072]: (ROUTEUR_1) Entering BACKUP STATE (init)
Apr 14 10:57:19 extrout1 Keepalived_vrrp[3072]: (ROUTEUR_1) Entering MASTER STATE

En parallèle, on va lancer un ping depuis une machine externe :

autremachineexterne# ping IP.FO

 

On lance maintenant sur extrout2 :

extrout2# service keepalived start

Dans le log, on voit qu’il est Slave :

Apr 14 10:59:55 extrout2 Keepalived_vrrp[1700]: (ROUTEUR_1) Entering BACKUP STATE (init)

 

On coupe sur extrout1:

extrout1# service keepalived stop

Extrout2 passe Master :

Apr 14 13:08:43 extrout2 Keepalived_vrrp[1700]: (ROUTEUR_1) Backup received priority 0 advertisement
Apr 14 13:08:44 extrout2 Keepalived_vrrp[1700]: (ROUTEUR_1) Entering MASTER STATE

 

On relance le 1

extrout1# service keepalived start

Extrout1 reprend le Master

Apr 14 13:21:16 extrout1 Keepalived_vrrp[3467]: (ROUTEUR_1) Entering BACKUP STATE (init)
Apr 14 13:21:16 extrout1 Keepalived_vrrp[3467]: (ROUTEUR_1) received lower priority (50) advert from 10.10.1.2 - discarding
Apr 14 13:21:17 extrout1 Keepalived_vrrp[3467]: (ROUTEUR_1) received lower priority (50) advert from 10.10.1.2 - discarding
Apr 14 13:21:18 extrout1 Keepalived_vrrp[3467]: (ROUTEUR_1) received lower priority (50) advert from 10.10.1.2 - discarding
Apr 14 13:21:19 extrout1 Keepalived_vrrp[3467]: (ROUTEUR_1) Entering MASTER STATE

Extrout2 repasse Slave

Apr 14 13:21:19 extrout2 Keepalived_vrrp[1700]: (ROUTEUR_1) Master received advert from 10.10.1.1 with higher priority 200, ours 50
Apr 14 13:21:19 extrout2 Keepalived_vrrp[1700]: (ROUTEUR_1) Entering BACKUP STATE

 

Et à chaque fois, le routeur Master récupère bien l’IP externe et l’IP interne flottantes. TADA !

# ip a
---> (sortie simplifiée)
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    inet 127.0.0.1/8 scope host lo
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    inet 10.10.1.1/24 brd 10.10.1.255 scope global eth1
    inet 10.10.1.254/32 scope global eth1
4: eth2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    inet 123.123.123.123/32 scope global eth2
5: eth0.99@eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    inet 10.99.1.1/24 brd 10.99.1.255 scope global eth0.99
6: eth0.250@eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    inet 10.250.1.1/24 brd 10.250.1.255 scope global eth0.250

et tout ceci avec peu de ping perdus.

 

On peut également pousser le test en coupant, non pas Keepalived mais carrément le routeur et d’une manière « violente » :

hyperv# virsh destroy extrout1

Destroy ne détruit pas la machine, je vous rassure, mais simule ce qu’on appelle dans le jargon : le pied dans la prise…

Bref, une fois extrout1 coupé, exrout2 devient bien Master, comme prévu.

Une fois validé, on peut relancer pour revenir en configuration de production en relançant extrout1

hyperv# virsh start extrout1

 

Bref, on y est, notre gateway en HA est opérationnelle, on peut revenir à notre feuille de route.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *