Redondance avec ExaBGP

Vincent Bernat

Plu­sieurs op­tions sont dis­po­nibles pour re­don­der un service :

  • Le ser­vice est placé der­rière un couple de ré­par­ti­teurs de charge qui vont dé­tec­ter toute dé­faillance. Il est alors né­ces­saire d’as­su­rer la dis­po­ni­bi­lité de cette nou­velle couche.
  • Les nœuds four­nis­sant le ser­vice peuvent re­prendre l’IP d’un autre nœud s’il est consi­déré comme dé­faillant (IP fai­lo­ver) à l’aide de pro­to­coles tels que VRRP1 ou CARP. Tous les nœuds doivent ce­pen­dant se trou­ver dans le même sous-réseau IP.
  • Les clients peuvent de­man­der à un tiers quels sont les nœuds dis­po­nibles. Ce tiers est le plus sou­vent le DNS : seuls les nœuds fonc­tion­nels sont an­non­cés dans l’en­re­gis­tre­ment DNS. Le dé­lai de mise à jour peut être par­ti­cu­liè­re­ment long en rai­son du mé­ca­nisme de cache.

Le plus sou­vent, ces trois tech­niques sont uti­li­sées en­semble : les ser­veurs sont pla­cés der­rière un couple de ré­par­ti­teurs de charge as­su­rant ainsi la re­don­dance et la ré­par­ti­tion des re­quêtes. Pour as­su­rer leur re­don­dance, ces ré­par­ti­teurs uti­lisent VRRP. L’en­semble est ré­pli­qué dans un autre da­ta­centre et un DNS « round-ro­bin » est uti­lisé pour as­su­rer re­don­dance et ré­par­ti­tion des da­ta­centres.

Il existe une qua­trième op­tion si­mi­laire à l’uti­li­sa­tion de VRRP mais qui se base sur le rou­tage dy­na­mique et n’im­plique pas de réunir les ser­vices dans un même sous-réseau :

  • Les nœuds in­diquent leur dis­po­ni­bi­lité à l’aide de BGP en an­non­çant les adresses IP qu’ils peuvent ser­vir. Chaque adresse est pon­dé­rée de fa­çon à ré­par­tir les IP parmi les serveurs.

Nous al­lons voir com­ment im­plé­men­ter cette op­tion à l’aide de ExaBGP, le cou­teau-suisse BGP, dans un labo basé sur KVM. Les sources de ce­lui-ci sont dis­po­nibles sur GitHub. ExaBGP 3.2.5 est né­ces­saire à son bon fonc­tionnement.

Mise à jour (05.2019)

Après la lec­ture de cet ar­ticle, je­tez un œil sur l’ar­ticle « Ré­par­ti­teur de charge à mul­tiples ni­veaux avec Li­nux ». Il dé­crit l’éla­bo­ra­tion d’une couche de ré­par­ti­tion de charge hau­te­ment dis­po­nible dont ExaBGP est l’un des com­posants.

Environnement#

Voici l’en­vi­ron­ne­ment de départ :

Labo avec 3 nœuds web
Réseau routé avec trois serveurs web

Configuration de BGP#

BGP est ac­tivé sur ER2 et ER3 afin d’échan­ger des routes avec des par­te­naires et des tran­si­taires (R1 dans notre cas). L’im­plé­men­ta­tion uti­li­sée est BIRD. Voici un frag­ment de sa confi­guration :

router id 1.1.1.2;

protocol static NETS { # ❶
  import all;
  export none;
  route 2001:db8::/40 reject;
}
protocol bgp R1 { # ❷
  import all;
  export where proto = "NETS";
  local as 64496;
  neighbor 2001:db8:1000::1 as 64511;
}
protocol bgp ER3 { # ❸
  import all;
  export all;
  next hop self;
  local as 64496;
  neighbor 2001:db8:1::3 as 64496;
}

En ❶, nous dé­cla­rons les routes que nous vou­lons ex­por­ter vers In­ter­net. Celles-ci se­ront in­con­di­tion­nel­le­ment an­non­cées. En­suite, en ❷, R1 est dé­claré comme voi­sin et nous lui en­voyons la route dé­fi­nie pré­cé­dem­ment. De plus, nous ac­cep­tons toutes les routes sou­mises par R1. En ❸, nous par­ta­geons toutes les in­for­ma­tions de rou­tage avec le rou­teur ju­meau ER3, via iBGP.

Configuration d’OSPF#

OSPF va per­mettre de dis­tri­buer les routes à l’in­té­rieur de l’AS. Ce pro­to­cole est ac­tivé sur ER2, ER3, DR6, DR7 et DR8. À titre d’exemple, voici les frag­ments per­ti­nents de la confi­gu­ra­tion de DR6 :

router id 1.1.1.6;
protocol kernel {
   persist;
   import none;
   export all;
}
protocol ospf INTERNAL {
  import all;
  export none;
  area 0.0.0.0 {
    networks {
      2001:db8:1::/64;
      2001:db8:6::/64;
    };
    interface "eth0";
    interface "eth1" { stub yes; };
  };
}

ER2 et ER3 in­jectent une route par dé­faut dans OSPF :

protocol static DEFAULT {
  import all;
  export none;
  route ::/0 via 2001:db8:1000::1;
}
filter default_route {
  if proto = "DEFAULT" then accept;
  reject;
}
protocol ospf INTERNAL {
  import all;
  export filter default_route;
  area 0.0.0.0 {
    networks {
      2001:db8:1::/64;
    };
    interface "eth1";
  };
}

Serveurs web#

Les ser­veurs web dis­posent sim­ple­ment d’une route par dé­faut sta­tique vers le rou­teur le plus proche. À no­ter qu’ils sont cha­cun dans un ré­seau IP dis­tinct : il n’est alors pas pos­sible de par­ta­ger une IP avec VRRP2.

Pour­quoi a-t-on placé ces ser­veurs sur des ré­seaux dif­fé­rents ? Ils peuvent être si­tués dans des da­ta­centres dif­fé­rents ou le ré­seau peut être com­plé­te­ment routé jusqu’à la couche d’accès.

Voyons com­ment mettre en œuvre BGP pour re­don­der ces serveurs.

Redondance avec ExaBGP#

ExaBGP est un ou­til pra­tique pour in­ter­fa­cer des scripts avec BGP. Ils peuvent alors re­ce­voir et an­non­cer des routes. ExaBGP s’oc­cupe de com­mu­ni­quer avec les rou­teurs. Les scripts lisent les routes re­çues sur l’en­trée stan­dard et en en­voient sur la sor­tie standard.

Vue d’ensemble#

Voici ce que nous al­lons construire :

Utilisation d'ExaBGP pour publier des services web
Haute disponibilité avec ExaBGP et deux serveurs de routes
  1. Trois adresses IP sont al­louées : 2001:​db8:30::1, 2001:​db8:30::2 et 2001:​db8:30::3. Elles sont dis­tinctes des IP réelles des serveurs.

  2. Chaque nœud va an­non­cer toutes les IP aux ser­veurs de routes. Je re­viens par la suite sur ces derniers.

  3. Chaque route an­non­cée contient une mé­trique qui per­met d’ai­der les ser­veurs de routes à choi­sir le ser­veur des­ti­na­tion. Les mé­triques sont choi­sies de fa­çon à ce qu’en si­tua­tion no­mi­nale, chaque IP est rou­tée vers un ser­veur différent.

  4. Les ser­veurs de routes (qui ne sont pas des rou­teurs) an­noncent en­suite les meilleures routes qu’ils ont ap­prises vers tous les rou­teurs du ré­seau, via BGP.

  5. Pour chaque adresse IP, chaque rou­teur a ap­pris la des­ti­na­tion à uti­li­ser. Les routes ap­pro­priées sont ins­tal­lées dans les tables de routage.

Voici les mé­triques res­pec­tives pour les routes an­non­cées par W1, W2 et W3 quand le fonc­tion­ne­ment est nominal :

Route W1 W2 W3 Best Backup
2001:db8:30::1 102 101 100 W3 W2
2001:db8:30::2 101 100 102 W2 W1
2001:db8:30::3 100 102 101 W1 W3

Configuration d’ExaBGP#

La confi­gu­ra­tion d’ExaBGP est très simple :

group rs {
  neighbor 2001:db8:1::4 {
    router-id 1.1.1.11;
    local-address 2001:db8:6::11;
    local-as 65001;
    peer-as 65002;
  }
  neighbor 2001:db8:8::5 {
    router-id 1.1.1.11;
    local-address 2001:db8:6::11;
    local-as 65001;
    peer-as 65002;
  }

  process watch-nginx {
      run /usr/bin/python /lab/healthcheck.py -s --config /lab/healthcheck-nginx.conf --start-ip 0;
  }
}

Le script vé­ri­fie le bon fonc­tion­ne­ment du ser­vice et de pu­blier les adresses IP vers les deux ser­veurs de routes. Il peut être lancé ma­nuel­le­ment de fa­çon à ob­ser­ver son fonc­tionnement :

$ python /lab/healthcheck.py --config /lab/healthcheck-nginx.conf --start-ip 0
INFO[healthcheck] send announces for UP state to ExaBGP
announce route 2001:db8:30::3/128 next-hop self med 100
announce route 2001:db8:30::2/128 next-hop self med 101
announce route 2001:db8:30::1/128 next-hop self med 102
[…]
WARNING[healthcheck] Check command was unsuccessful: 7
INFO[healthcheck] Output of check command:  curl: (7) Failed connect to ip6-localhost:80; Connection refused
WARNING[healthcheck] Check command was unsuccessful: 7
INFO[healthcheck] Output of check command:  curl: (7) Failed connect to ip6-localhost:80; Connection refused
WARNING[healthcheck] Check command was unsuccessful: 7
INFO[healthcheck] Output of check command:  curl: (7) Failed connect to ip6-localhost:80; Connection refused
INFO[healthcheck] send announces for DOWN state to ExaBGP
announce route 2001:db8:30::3/128 next-hop self med 1000
announce route 2001:db8:30::2/128 next-hop self med 1001
announce route 2001:db8:30::1/128 next-hop self med 1002

Lorsque le ser­vice de­vient in­dis­po­nible, la si­tua­tion est dé­tec­tée par le script qui va ré­es­sayer plu­sieurs fois avant d’aban­don­ner. Les adresses IP sont alors an­non­cées avec une mé­trique plus éle­vée et le ser­vice sera alors routé vers un autre nœud (ce­lui qui pu­blie 2001:​db8:30::​3/128 avec une mé­trique de 101).

Ce script fait dé­sor­mais par­tie de ExaBGP et doit être in­vo­qué ainsi :

$ python -m exabgp healthcheck --config /lab/healthcheck-nginx.conf --start-ip 0

Les serveurs de routes#

Nous au­rions pu connec­ter ExaBGP di­rec­te­ment aux rou­teurs. Tou­te­fois, si nous avions une ving­taine de rou­teurs et une di­zaine de ser­veurs web, il fau­drait main­te­nir en­vi­ron 200 ses­sions. Les ser­veurs de routes ont trois rôles :

  1. Ré­duire le nombre de ses­sions BGP entre les équi­pe­ments. Moins de confi­gu­ra­tion, moins d’erreurs.

  2. Évi­ter de mo­di­fier la confi­gu­ra­tion des rou­teurs à chaque ajout de service.

  3. Sé­pa­rer les dé­ci­sions de rou­tage (prises par les ser­veurs de routes) du pro­ces­sus de rou­tage (ef­fec­tué par les routeurs).

Une ques­tion que l’on peut lé­gi­ti­me­ment se po­ser est : « pour­quoi ne pas uti­li­ser OSPF ? ».

  • OSPF pour­rait être ac­tivé sur chaque ser­veur web et les adresses pu­bliées via ce pro­to­cole. Ce­pen­dant, OSPF a plu­sieurs li­mi­ta­tions : on ne peut pas ajou­ter des par­ti­ci­pants à l’in­fini, toutes les to­po­lo­gies ne sont pas pos­sibles, il est dif­fi­cile de fil­trer les routes et une er­reur de confi­gu­ra­tion peut fa­ci­le­ment af­fec­ter l’en­semble du ré­seau. Ainsi, il est consi­déré comme sou­hai­table de li­mi­ter OSPF à des routeurs.

  • Les routes ap­prises par les ser­veurs de routes pour­raient être in­jec­tées di­rec­te­ment dans OSPF. Ce se­rait pra­tique au ni­veau de la confi­gu­ra­tion puisqu’il ne se­rait plus né­ces­saire de confi­gu­rer les ad­ja­cences. Sur le pa­pier, OSPF sait uti­li­ser un champ « next-hop ». Tou­te­fois, je n’ai trouvé au­cun moyen d’in­jec­ter le champ cor­res­pon­dant de BGP dans ce­lui-ci. Le BGP next-hop est ré­solu lo­ca­le­ment en uti­li­sant les in­for­ma­tions is­sues d’OSPF. Si le ré­sul­tat est in­jecté dans OSPF, les rou­teurs en­voient le tra­fic à des­ti­na­tion des IP de ser­vice vers RS4.

Voyons com­ment confi­gu­rer les ser­veurs de routes. RS4 uti­lise BIRD tan­dis que RS5 uti­lise Quagga. L’uti­li­sa­tion de deux im­plé­men­ta­tions dif­fé­rentes per­met d’être ré­si­lient aux bugs qui peuvent af­fec­ter une im­plé­mentation.

Configuration de BIRD#

Il y a deux par­ties dans la confi­gu­ra­tion de BGP : les ses­sions BGP avec les nœuds ExaBGP et celles avec les rou­teurs. Voici la confi­gu­ra­tion concer­nant cette dernière :

template bgp INFRABGP {
  export all;
  import none;
  local as 65002;
  rs client;
}
protocol bgp ER2 from INFRABGP {
  neighbor 2001:db8:1::2 as 65003;
}
protocol bgp ER3 from INFRABGP {
  neighbor 2001:db8:1::3 as 65003;
}
protocol bgp DR6 from INFRABGP {
  neighbor 2001:db8:1::6 as 65003;
}
protocol bgp DR7 from INFRABGP {
  neighbor 2001:db8:1::7 as 65003;
}
protocol bgp DR8 from INFRABGP {
  neighbor 2001:db8:1::8 as 65003;
}

Le nu­méro d’AS uti­lisé pour les ser­veurs de routes est 65002 tan­dis que 65003 est uti­lisé pour les rou­teurs (et 65001 pour les ser­veurs). Ces AS sont pris dans le lot des nu­mé­ros ré­ser­vés pour un usage privé par la RFC 6996.

Toutes les routes connues du ser­veur de routes sont ex­por­tées vers les rou­teurs mais au­cune route n’est ac­cep­tée de ceux-ci.

Voyons la deuxième partie :

# Only import loopback IPs
filter only_loopbacks { # ❶
  if net ~ [ 2001:db8:30::/64{128,128} ] then accept;
  reject;
}

# General template for an EXABGP node
template bgp EXABGP {
  local as 65002;
  import filter only_loopbacks; # ❷
  export none;
  route limit 10; # ❸
  rs client;
  hold time 6; # ❹
  multihop 10;
}

protocol bgp W1 from EXABGP {
  neighbor 2001:db8:6::11 as 65001;
}
protocol bgp W2 from EXABGP {
  neighbor 2001:db8:7::12 as 65001;
}
protocol bgp W3 from EXABGP {
  neighbor 2001:db8:8::13 as 65001;
}

Pour as­su­rer une bonne sé­pa­ra­tion des res­pon­sa­bi­li­tés, nous sommes un peu plus poin­tilleux. En com­bi­nant ❶ et ❷, seules les adresses IP de loop­back in­cluses dans le bon sous-ré­seau sont ac­cep­tées. Au­cun ser­veur ne doit pou­voir in­jec­ter des routes ar­bi­traires dans notre ré­seau. Grâce à ❸, le nombre de routes qu’un même ser­veur peut an­non­cer est limité.

Avec ❹, nous ré­dui­sons le temps de dé­tec­tion d’in­dis­po­ni­bi­lité (hold time) de 240 à 6 se­condes. C’est par­ti­cu­liè­re­ment im­por­tant pour être ca­pable de ré­agir ra­pi­de­ment si un ser­veur de­vient in­dis­ponible.

Configuration de Quagga#

La confi­gu­ra­tion de Quagga est un peu plus ver­beuse mais stric­te­ment équi­valente :

router bgp 65002 view EXABGP
 bgp router-id 1.1.1.5
 bgp log-neighbor-changes
 no bgp default ipv4-unicast

 neighbor R peer-group
 neighbor R remote-as 65003
 neighbor R ebgp-multihop 10

 neighbor EXABGP peer-group
 neighbor EXABGP remote-as 65001
 neighbor EXABGP ebgp-multihop 10
 neighbor EXABGP timers 2 6
!
 address-family ipv6

 neighbor R activate
 neighbor R soft-reconfiguration inbound
 neighbor R route-server-client
 neighbor R route-map R-IMPORT import
 neighbor R route-map R-EXPORT export
 neighbor 2001:db8:1::2 peer-group R
 neighbor 2001:db8:1::3 peer-group R
 neighbor 2001:db8:1::6 peer-group R
 neighbor 2001:db8:1::7 peer-group R
 neighbor 2001:db8:1::8 peer-group R

 neighbor EXABGP activate
 neighbor EXABGP soft-reconfiguration inbound
 neighbor EXABGP maximum-prefix 10
 neighbor EXABGP route-server-client
 neighbor EXABGP route-map RSCLIENT-IMPORT import
 neighbor EXABGP route-map RSCLIENT-EXPORT export
 neighbor 2001:db8:6::11 peer-group EXABGP
 neighbor 2001:db8:7::12 peer-group EXABGP
 neighbor 2001:db8:8::13 peer-group EXABGP

 exit-address-family
!
ipv6 prefix-list LOOPBACKS seq 5 permit 2001:db8:30::/64 ge 128 le 128
ipv6 prefix-list LOOPBACKS seq 10 deny any
!
route-map RSCLIENT-IMPORT deny 10
!
route-map RSCLIENT-EXPORT permit 10
  match ipv6 address prefix-list LOOPBACKS
!
route-map R-IMPORT permit 10
!
route-map R-EXPORT deny 10
!

L’uti­li­sa­tion d’une vue per­met d’évi­ter d’ins­tal­ler les routes dans le noyau3.

Les routeurs#

La confi­gu­ra­tion de BIRD sur les rou­teurs est as­sez simple :

# BGP with route servers
protocol bgp RS4 {
  import all;
  export none;
  local as 65003;
  neighbor 2001:db8:1::4 as 65002;
  gateway recursive;
}
protocol bgp RS5 {
  import all;
  export none;
  local as 65003;
  neighbor 2001:db8:8::5 as 65002;
  multihop 4;
  gateway recursive;
}

Il est im­por­tant d’uti­li­ser gateway recursive car la plu­part du temps, le rou­teur ne peut pas at­teindre la des­ti­na­tion di­rec­te­ment. Dans ce cas, par dé­faut, BIRD uti­lise l’adresse IP du rou­teur à l’ori­gine de l’an­nonce (le ser­veur de routes).

Tests#

Vé­ri­fions que tout fonc­tionne comme at­tendu. Voici ce que voit RS5 :

# show ipv6  bgp
BGP table version is 0, local router ID is 1.1.1.5
Status codes: s suppressed, d damped, h history, * valid, > best, i - internal,
              r RIB-failure, S Stale, R Removed
Origin codes: i - IGP, e - EGP, ? - incomplete

   Network          Next Hop            Metric LocPrf Weight Path
*  2001:db8:30::1/128
                    2001:db8:6::11         102             0 65001 i
*>                  2001:db8:8::13         100             0 65001 i
*                   2001:db8:7::12         101             0 65001 i
*  2001:db8:30::2/128
                    2001:db8:6::11         101             0 65001 i
*                   2001:db8:8::13         102             0 65001 i
*>                  2001:db8:7::12         100             0 65001 i
*> 2001:db8:30::3/128
                    2001:db8:6::11         100             0 65001 i
*                   2001:db8:8::13         101             0 65001 i
*                   2001:db8:7::12         102             0 65001 i

Total number of prefixes 3

Par exemple, le tra­fic vers 2001:​db8:30::2 doit être routé via 2001:​db8:7::12 (qui est W2). Les autres IP sont af­fec­tées à W1 et W3.

RS4 voit exac­te­ment la même chose4 :

$ birdc6 show route
BIRD 1.3.11 ready.
2001:db8:30::1/128 [W3 22:07 from 2001:db8:8::13] * (100/20) [AS65001i]
                   [W1 23:34 from 2001:db8:6::11] (100/20) [AS65001i]
                   [W2 22:07 from 2001:db8:7::12] (100/20) [AS65001i]
2001:db8:30::2/128 [W2 22:07 from 2001:db8:7::12] * (100/20) [AS65001i]
                   [W1 23:34 from 2001:db8:6::11] (100/20) [AS65001i]
                   [W3 22:07 from 2001:db8:8::13] (100/20) [AS65001i]
2001:db8:30::3/128 [W1 23:34 from 2001:db8:6::11] * (100/20) [AS65001i]
                   [W3 22:07 from 2001:db8:8::13] (100/20) [AS65001i]
                   [W2 22:07 from 2001:db8:7::12] (100/20) [AS65001i]

Voyons DR6 :

$ birdc6 show route
2001:db8:30::1/128 via fe80::5054:56ff:fe6e:98a6 on eth0 * (100/20) [AS65001i]
                   via fe80::5054:56ff:fe6e:98a6 on eth0 (100/20) [AS65001i]
2001:db8:30::2/128 via fe80::5054:60ff:fe02:3681 on eth0 * (100/20) [AS65001i]
                   via fe80::5054:60ff:fe02:3681 on eth0 (100/20) [AS65001i]
2001:db8:30::3/128 via 2001:db8:6::11 on eth1 * (100/10) [AS65001i]
                   via 2001:db8:6::11 on eth1 (100/10) [AS65001i]

Ainsi, 2001:​db8:30::3 est bien routé vers W1 qui se trouve di­rec­te­ment der­rière DR6. Les deux autres IP sont en­voyées vers une autre par­tie du ré­seau via les adresses ap­prises par OSPF.

Stop­pons nginx sur W1. Quelques se­condes plus tard, DR6 ap­prend de nou­velles routes :

$ birdc6 show route
2001:db8:30::1/128 via fe80::5054:56ff:fe6e:98a6 on eth0 * (100/20) [AS65001i]
                   via fe80::5054:56ff:fe6e:98a6 on eth0 (100/20) [AS65001i]
2001:db8:30::2/128 via fe80::5054:60ff:fe02:3681 on eth0 * (100/20) [AS65001i]
                   via fe80::5054:60ff:fe02:3681 on eth0 (100/20) [AS65001i]
2001:db8:30::3/128 via fe80::5054:56ff:fe6e:98a6 on eth0 * (100/20) [AS65001i]
                   via fe80::5054:56ff:fe6e:98a6 on eth0 (100/20) [AS65001i]

Démo#

Voici une vi­déo mon­trant un aperçu du lab en fonc­tionnement :