Pour fixer les idées, disons que je dispose de la plage 192.0.2.72/29, c'est-à-dire les adresses de 192.0.2.72 à 192.0.2.79.
Le réflexe naturel serait de les utiliser directement sur le réseau local en se passant d'adresses RFC 1918. Malheureusement, dans mon cas, j'ai tout simplement trop de machines. J'ai donc commencé par séparer mon réseau local en deux segments Ethernet (en fait deux VLAN, mais ça n'a pas d'importance pour ce qui nous occupe), l'un en adressage privé, et l'autre pour mes adresses publiques.
C'est bon alors, l'article est fini ? Hé non. Si cette solution fonctionne à peu près, elle a l'immense inconvénient de bloquer deux de nos huit adresses pour le réseau et la diffusion. Ça fait 25% de gaspillage, et ça heurte ma fibre écolo. Du coup, on va essayer de faire mieux, et ça aura en plus le bon goût d'être plus simple, même si ce n'est pas évident à première vue.
1. Notre réseau
Nous allons monter une petite maquette avec trois machines, la généralisation est laissée en exercice au lecteur.
La première de ces machines fait office de routeur et s'appelle donc, fort logiquement, routeur. Elle dispose d'une interface Ethernet, eth0, sur le réseau local, et d'une autre interface raccordée à Internet d'une façon ou d'une autre. Je ne vais pas m'étendre sur la configuration du routage ou de la traduction d'adresses, il y a plein de docs là-dessus et ce n'est de toute façon pas le sujet ; on va juste supposer que le routeur est configuré « comme il faut » pour remplir sa fonction.
Les deux autres machines s'appelleront serveur1 et serveur2, et je vais leur affecter les adresses IP 192.0.2.72 et 192.0.2.73, respectivement.
« Diantre ! », vous exclamerez-vous, « Mais il me prend pour une bille ! 192.0.2.72, c'est l'adresse du réseau !». Oui, mais non. Ce serait certes le cas si nous utilisions un réseau à diffusion, typiquement Ethernet, mais pas dans le cas présent. Nos huit adresses IP seront rigoureusement interchangeables, ce qui est tout de même le but de la manip'.
À ce sujet, vous noterez par la suite que le routeur lui-même n'a pas besoin d'utiliser une adresse publique. Et en voilà encore une de gagnée !
Pour fixer les idées, notre réseau local utilisera la plage 10.0.5.0/24, le routeur aura l'adresse 10.0.5.1 et les serveurs seront en 10.0.5.11 et 10.0.5.12. Pour les besoins de l'exemple les trois machines tourneront sous Debian Wheezy ; à vous de transposer pour votre système d'exploitation préféré.
2. L'interface dummy
Le noyau Linux permet de monter une interface Ethernet bidon, qui est essentiellement une variante de l'interface de boucle locale (loopback) que vous connaissez probablement déjà. Sans entrer dans les détails, la boucle locale de Linux a parfois des comportements un peu inhabituels, alors que l'interface bidon ressemble plus à une interface Ethernet ; c'est donc elle qu'on utilise habituellement pour monter une adresse qu'on ne veut pas rendre directement accessible à l'extérieur de la machine. Toujours sans entrer dans les détails, si vous utilisez un système d'exploitation différent pour vos serveurs, par exemple un BSD, vous pouvez parfaitement utiliser l'interface lo0.
On va donc commencer par ajouter une interface bidon à nos serveurs pour y affecter leurs adresses publiques. Je sais, monter une adresse publique sur une interface inaccessible de l'extérieur peut sembler étrange ; pas d'inquiétude, on parlera routage d'ici trois paragraphes.
Voici donc la configuration de l'interface bidon de serveur1 (dans le fichier /etc/network/interfaces, je vous laisse trouver l'équivalent pour votre distribution) ; il faudra bien sûr écrire l'équivalent sur serveur2, que je ne vais pas détailler ici.
auto dummy0
iface dummy0 inet static
address 192.0.2.72
netmask 255.255.255.255
On peut maintenant activer notre interface :
root@serveur1:~# ifup dummy0
root@serveur1:~# ip a sh dev dummy0
3: dummy0: <BROADCAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN
link/ether 06:27:e7:f8:df:59 brd ff:ff:ff:ff:ff:ff
inet 192.0.2.72/32 brd 192.0.2.72 scope global dummy0
inet6 fe80::427:e7ff:fef8:df59/64 scope link
valid_lft forever preferred_lft forever
Voilà, notre serveur connaît son adresse publique ! Par contre, le routeur ne sait bien entendu pas comment la joindre, il va donc falloir lui indiquer :
root@routeur:~# ip r add 192.0.2.72/32 via 10.0.5.11
root@routeur:~# fping 192.0.2.72
192.0.2.72 is alive
Voilà, je vous avais dit qu'on allait parler routage, c'est fait. Enfin, presque, reste à rendre la configuration persistante ; sur une Debian, ça se fait en installant le paquet ifupdown-extra et en ajoutant les routes au fichier /etc/network/routes :
root@routeur:~# cat >> /etc/network/routes
192.0.2.72 255.255.255.255 10.0.5.11 any
192.0.2.73 255.255.255.255 10.0.5.12 any
root@routeur:~# /etc/init.d/networking-routes start
[ ok ] Configuring network routes...done.
root@routeur:~# ip r sh dev eth0
10.0.5.0/24 proto kernel scope link src 10.0.5.1
192.0.2.72 via 10.0.5.11
192.0.2.73 via 10.0.5.12
C'est fini alors, on peut s'arrêter là ? Non, toujours pas. Pour comprendre pourquoi, on va aller faire un tour sur serveur2 et voir s'il peut joindre l'adresse publique de serveur1.
root@serveur2:~# fping 192.0.2.72
192.0.2.72 is alive
Jusqu'ici tout va bien. Et si on creuse un peu ?
root@serveur2:~# traceroute -I 192.0.2.72
traceroute to 192.0.2.72 (192.0.2.72), 30 hops max, 60 byte packets
1 10.0.5.1 (10.0.5.1) 1.556 ms 1.526 ms 1.350 ms
2 192.0.2.72 (192.0.2.72) 2.126 ms 2.118 ms 2.104 ms
Les paquets passent par le routeur, alors que les deux serveurs sont raccordés au même segment Ethernet. Ça marche, mais ce n'est pas optimal.
On peut bien sûr ajouter une route statique de serveur2 vers serveur1, et réciproquement. Mais si le nombre de machines augmente, ça va finir par faire beaucoup de routes, et du boulot pour tout mettre à jour à chaque fois qu'on voudra déplacer une adresse publique. Or, je suis administrateur système, du coup je suis fainéant, c'est dans la fiche de poste. Alors on va arrêter de gérer le routage à la main, on va configurer les machines pour qu'elles le fassent elles-mêmes et on va les regarder bosser, non mais !
3. OSPF
Il existe un certain nombre de protocoles de routage dynamiques qui peuvent faire le boulot à notre place. Vous me direz que ces protocoles sont conçus pour synchroniser la configuration de routeurs, pas de serveurs. Il se trouve que dans le cas présent, chaque serveur joue aussi le rôle de routeur entre notre réseau local et le /32 qui contient son adresse publique. On est dans les clous, on peut continuer, bonne nouvelle !
J'ai choisi d'utiliser le protocole OSPF, qui est bien adapté à un réseau local avec un nombre fluctuant de routeurs. Il s'agit d'un protocole de couche 4, qui fonctionne donc directement sur IP à côté de TCP et UDP. Plus précisément, nous allons utiliser sa version 2, qui fonctionne sur IPv4 et qui est définie dans la RFC 2328. Je mentionnerai la version 3 un peu plus tard, quand j'aborderai la question d'IPv6, mais pour le moment, ne nous dispersons pas et restons sur OSPFv2.
Avant d'entrer dans le vif du sujet, une description rapide du protocole. Je me contenterai du cas qui nous occupe, c'est-à-dire un réseau à diffusion tout simple ; OSPF peut aussi fonctionner sur des liens point à point, ou sur des réseaux à topologie plus compliquée, mais nous n'utiliserons pas cette possibilité ici.
Le principe de base est relativement simple. Chaque routeur commence par faire l'inventaire des réseaux auxquels il est directement connecté. Il s'annonce ensuite en multicast sur chacun de ces réseaux pour découvrir ses voisins et établir une relation d'adjacence avec eux. Chaque routeur échange ensuite les informations de topologie dont il dispose avec les routeurs adjacents. Pour finir, une fois que tous les routeurs ont une vue d'ensemble de la topologie du réseau, chacun génère sa table de routage en utilisant l'algorithme de Dijkstra.
La découverte des voisins et l'établissement des relations d'adjacence mérite un peu d'approfondissement. Chaque routeur émet régulièrement des paquets OSPF hello sur le groupe multicast 224.0.0.5. Les routeurs d'un même domaine de diffusion (donc, dans notre cas, toutes nos machines) élisent un routeur désigné (DR) et un routeur désigné de secours (BDR). Ensuite, tous les routeurs du domaine établissent un lien d'adjacence avec le DR, et le BDR se tient prêt à prendre le relais en cas de disparition du DR. Cette optimisation permet de réduire le nombre de liens d'adjacence et donc, le trafic OSPF global ; elle n'a par contre aucun impact sur le calcul de la table de routage.
Pour finir, quelques notions techniques que nous verrons passer un peu plus loin.
Un réseau important peut être segmenté en plusieurs zones, numérotées sur 32 bits. L'usage veut qu'on note les numéros de zone sous la forme de quatre octets séparés par des points, comme une adresse IPv4. La zone 0.0.0.0 est privilégiée ; c'est la dorsale à laquelle toutes les autres zones sont directement reliées. Cette segmentation permet de rendre les zones autonomes, typiquement pour éviter qu'un lien qui bagotte force tous les routeurs du réseau à recalculer leurs routes ; un routeur qui relie deux zones entre elles peut agréger ses routes pour cacher la topologie interne de chaque zone à sa voisine. Dans le cas de notre petit réseau tout à plat, nous n'utiliserons que la zone 0.0.0.0.
D'autre part, chaque routeur dispose d'un identifiant unique sur le réseau, là encore un entier sur 32 bits noté comme une adresse IPv4. On utilise traditionnellement une des adresses IP du routeur comme identifiant, ce qui permet d'assurer cette unicité (ou presque, après tout, rien n'interdit de monter plusieurs fois la même adresse IP sur des réseaux différents).
4. BIRD
Il existe au moins quatre implémentations libres d'OSPF, à savoir Quagga, BIRD, XORP et OpenOSPFD. Mon choix s'est porté sur BIRD, qui est à la fois léger, stable et sans surprises pour un admin système. Ce que je veux dire avec ce dernier critère, c'est que BIRD se configure comme un démon Unix et pas comme un routeur : on édite un fichier de config, on envoie le signal SIGHUP au démon bird et ce dernier relit le fichier.
Debian distribue BIRD et une version presque à jour est disponible dans les backports ; c'est celle-ci que nous allons installer. Si votre système ne fournit pas de paquet BIRD, ou si vous voulez absolument installer la dernière version, vous pouvez aller la chercher sur le site de référence (http://bird.network.cz/) ; ici, nous nous contenterons du paquet Debian.
root@routeur:~# apt-get install -t wheezybackports bird
Deux petites observations. Tout d'abord, si votre apt-get est configuré pour installer les paquets recommandés (ce qu'il fait par défaut sous Debian Wheezy), il va aussi vous installer un paquet bird6 ; il s'agit du pendant IPv6 de BIRD, dont nous parlerons vers la fin de l'article. D'autre part, Debian étant toujours Debian, le démon bird est lancé sitôt après l'installation ; toutefois, sa configuration par défaut fait très exactement rien. Nous allons donc nous empresser de la remplacer par la nôtre.
4.1. Configuration du routeur
Le démon bird se configure dans le fichier /etc/bird.conf (encore une fois, ça peut varier d'un système d'exploitation à l'autre). Voici le contenu de ce fichier sur routeur :
router id 10.0.5.1;
protocol device {
scan time 10;
}
protocol kernel {
learn;
persist;
scan time 1;
import all;
export all;
}
protocol ospf PubIP {
area 0.0.0.0 {
interface "eth0" { };
};
import all;
export none;
}
Nous avons déjà vu la signification de la première ligne, c'est l'identifiant du routeur. Nous avons ensuite trois blocs qui servent à configurer trois protocoles de routage. Pourquoi trois, alors que jusqu'ici je n'ai parlé que d'OSPF ? Voyons ça plus en détails.
Le protocole device sert à faire l'inventaire des interfaces réseau de la machine. Ici, nous le configurons pour rafraîchir cette liste toutes les 10 secondes ; sous Linux, le noyau est de toute façon capable d'aviser BIRD des changements en temps réel, cet inventaire périodique sert juste au cas où un message du noyau se perdrait en route. Sur nos machines à la configuration réseau à peu près statique, on peut sans risque choisir un intervalle de rafraîchissement très élevé.
Le protocole kernel permet à BIRD de synchroniser sa table de routage avec celle du noyau. Nous avons déjà vu la signification de l'option scan time. L'option learn indique à BIRD qu'il doit tenir compte des routes de toute origine (par exemple, celles qui auraient été établies par un autre démon de routage) ; quant à l'option persist, elle indique à BIRD de laisser ses routes en place lorsqu'il s'arrête au lieu de nettoyer la table de routage. Enfin, les options import et export permettent de contrôler, respectivement, la liste des routes que BIRD apprend du noyau et celles qu'il lui communique. Dans le cas présent, on choisit de communiquer toutes les routes dans les deux sens ; on peut remplacer le mot-clé all par un filtre, je vous laisse consulter la documentation de BIRD pour vous faire une idée des possibilités.
Une petite note au passage : si nous n'avons ici qu'une seule instance du protocole kernel, on peut tout à fait en définir plusieurs, qui correspondront à différentes tables de routage du noyau. Cette possibilité n'existe bien entendu que sur les systèmes dont le noyau supporte plusieurs tables de routage, ce qui est le cas de Linux.
Enfin, nous entrons dans le vif du sujet avec le protocole ospf. Nous pouvons en définir plusieurs instances ; ici, nous nous contenterons d'une seule, identifiée par l'étiquette PubIP. Nous définissons une seule zone OSPF, la dorsale 0.0.0.0, à laquelle nous raccordons l'interface eth0. Nous connaissons déjà les options import et export ; ici, l'idée est que le routeur écoute les annonces des serveurs, mais n'annonce rien lui-même, puisque les routes par défaut des serveurs lui enverront de toute façon tout le trafic.
4.2. Configuration des serveurs
La configuration des serveurs est très proche de celle du routeur ; examinons rapidement les différences. Je passe sur l'identifiant ; nous prendrons les adresses IP privées des machines, histoire de ne pas devoir en changer le jour où on déplacera les adresses publiques. La configuration des protocoles device et kernel est identique ; reste à voir le protocole ospf.
protocol ospf PubIP {
area 0.0.0.0 {
interface "eth0" { };
interface "dummy0" { stub yes; };
};
import all;
export all;
}
La directive export prend la valeur all : on veut annoncer notre adresse publique, on doit donc exporter nos routes en OSPF. Notez que la directive import reste à all ; en effet, un de nos objectifs est que chaque serveur puisse apprendre les routes vers les adresses publiques de ses voisins. Et puisqu'il est question d'exporter des routes, on configure notre instance d'ospf pour ajouter l'interface dummy0 à la dorsale, afin d'y annoncer les adresses configurées pour cette interface. Le drapeau stub indique qu'aucun trafic OSPF ne passera sur cette interface, on se contente de l'annoncer sur le reste du réseau.
Une fois bird configuré et lancé, on peut accéder à son interface de contrôle via la commande birdc. Une fois dans l'interface de contrôle, la touche ? permet d'accéder à l'aide en ligne, je vous laisse la découvrir par vous-même. Dans notre cas, on peut au moins s'en servir pour examiner les relations d'adjacence ; si les démons tournent depuis un petit moment (plus de quelques secondes, le temps que les instances OSPF négocient entre elles), le résultat devrait ressembler à ça :
root@serveur2:~# birdc
BIRD 1.3.10 ready.
bird> sh ospf neighbors
PubIP:
Router ID Pri State DTime Interface Router IP
10.0.5.1 1 full/bdr 00:39 eth0 10.0.5.1
10.0.5.11 1 full/dr 00:39 eth0 10.0.5.11
Notez les états full/dr et full/bdr, qui indiquent respectivement le DR et le BDR ; les autres routeurs OSPF du réseau sont normalement dans l'état full/other.
On peut vérifier que chaque serveur a bien une route vers l'IP publique de son voisin :
root@serveur2:~# ip r sh
default via 10.0.5.1 dev eth0
10.0.5.0/24 dev eth0 proto kernel scope link src 10.0.5.12
192.0.2.72 via 10.0.5.11 dev eth0 proto bird
Et sur le routeur :
root@routeur:~# ip r sh dev eth0
10.0.5.0/24 proto kernel scope link src 10.0.5.1
192.0.2.72 via 10.0.5.11 proto bird
192.0.2.73 via 10.0.5.12 proto bird
Mission accomplie ! Avant de terminer cet article, je voudrais juste vous donner quelques pistes pour aller un peu plus loin.
5. Pour aller plus loin
5.1. Un peu de haute dispo
Et si je monte la même adresse IP sur les deux serveurs, il se passe quoi ? Essayons pour voir, en ajoutant l'adresse 192.0.2.72 sur l'interface dummy0 de serveur2.
root@routeur:~# ip r sh dev eth0
10.0.5.0/24 proto kernel scope link src 10.0.5.1
192.0.2.72 via 10.0.5.12 proto bird
192.0.2.73 via 10.0.5.12 proto bird
Arrêtons maintenant serveur2 brutalement ; après une trentaine ou une quarantaine de secondes le temps qu'OSPF converge, on retrouve notre route vers serveur1.
root@routeur:~# ip r sh dev eth0
10.0.5.0/24 proto kernel scope link src 10.0.5.1
192.0.2.72 via 10.0.5.11 proto bird
Dans cette configuration, on a une forme limitée de haute disponibilité. Les possibilités sont moindres qu'avec des outils spécialisés genre keepalived, mais une fois notre routage OSPF en place, c'est gratuit.
5.2. Et la sécurité dans tout ça ?
Le protocole OSPF, tel que défini dans la RFC 2328, permet d'authentifier les paquets OSPF au moyen d'une empreinte MD5. L'empreinte en question est générée à partir du paquet lui-même et d'une clé partagée par les routeurs OSPF adjacents. La RFC 5709 prévoit l'utilisation des algorithmes de la famille SHA à la place de MD5, mais à ma connaissance, cette extension n'est pas implantée dans BIRD.
Pour activer ce mode d'authentification, il suffit d'indiquer la clé partagée dans la définition de l'interface concernée dans le fichier bird.conf ; l'instance OSPF utilisera alors cette clé partagée pour signer tout le trafic OSPF sur cette interface, et refusera les paquets qui ne sont pas signés avec la même clé. Notez que cette signature n'interdit pas à un attaquant d'écouter le trafic OSPF, elle empêche juste l'injection de routes pirates.
interface "eth0" {
authentication cryptographic;
password "maclepartageequejai";
};
BIRD permet d'affiner un peu cette configuration, notamment de prévoir un changement de mot de passe à une date donnée ; je vous laisse lire la documentation pour en savoir plus.
5.3. Et IPv6 alors ?
C'est vrai, quoi, on est au XXIème siècle et pas un mot sur IPv6 ? C'est simplement pour une raison pratique : les adresses IPv6 sont tellement abondantes que je n'ai jamais rencontré le problème de base de l'article (à savoir « j'ai moins d'adresses publiques que de machines »).
BIRD est parfaitement utilisable en IPv6. Par contre, le même binaire bird parle IPv4 ou IPv6, mais pas les deux. Le choix se fait à la compilation. La plupart des distributions fournissent donc deux binaires, bird pour IPv4 et bird6 pour IPv6 (Debian comporte deux paquets séparés, bird et bird6).
La configuration de bird6 est très similaire à ce qu'on a vu en IPv4 ; il faut juste faire attention à deux petites choses.
D'une part, j'insiste sur le fait que les numéros de zones et les identifiants de routeurs sont des entiers sur 32 bits et pas des adresses IP, la notation ne doit pas vous induire en erreur. Ils restent exprimés sur 32 bits même en IPv6. Ça a un petit effet de bord en pratique : si en IPv4 BIRD est capable de générer un identifiant de routeur lui-même (il utilise la plus petite adresse IPv4 portée par une interface autre que la boucle locale), en IPv6 l'option router id est obligatoire.
D'autre part, l'authentification cryptographique n'existe pas en IPv6. Mais comment donc, pas de sécurité ? Les concepteurs d'OSPFv3 sont partis du principe qu'une pile IPv6 comportait obligatoirement une implantation d'IPSEC et donc, qu'on n'avait qu'à s'en servir pour chiffrer le trafic OSPF ; non mais c'est vrai, c'est pas parce que ça fait mal à la tête qu'on ne doit pas le faire. La réalité de la vraie vie étant ce qu'elle est (surtout depuis la RFC 6434, qui supprime l'obligation d'implanter IPSEC), j'ai bien peur que la sécurité de beaucoup de déploiements OSPFv3 se limite à du filtrage par adresse d'émetteur.
Références
- Algorithme de Dijkstra : http://fr.wikipedia.org/wiki/Algorithme_de_Dijkstra
- BIRD : http://bird.network.cz/
- OSPFv2 : http://tools.ietf.org/html/rfc2328
- OSPFv3 : http://tools.ietf.org/html/rfc5340
- Authentification cryptographique HMAC-SHA pour OSPFv2 : http://tools.ietf.org/html/rfc5340