Routage sélectif vers plusieurs connexions internet

From Le Wiki de debian-fr.xyz
Jump to navigation Jump to search

On dispose de plusieurs accès internet distincts, et l'on souhaite pouvoir rediriger (re-router) arbitrairement certaines connexions réseau vers l'un ou l'autre de ces accès.

Autrement dit, nous allons gérer de manière sélective le load-balancing entre plusieurs accès à internet. Comme nous le verrons par la suite, les critères déterminant quelle connexion doit utiliser quel accès ne sont qu'une toute petite partie du problème.


Un autre aspect important, le fail-over en cas d'indisponibilité d'un des accès, n'est pas traité ici : on considérera que les stratégies de routage doivent s'appliquer de manière stricte (si une connexion est censée utiliser un accès indisponible elle sera tout simplement refusée). Ceci peut assez facilement être rajouté par la suite en rajoutant des scripts (attachés aux évènements up et down des interfaces ou surveillant en tâche de fond l'état de santé des accès internet) qui seront chargés de modifier dynamiquement la configuration de routage et d'iptables en fonction des interfaces disponibles. Par souci de simplicité nous omettrons cette partie qui est laissée comme exercice pour le lecteur.


Ces accès internet peuvent prendre de nombreuses formes : ADSL, fibre optique, Wifi, modem, tunnel IP, PPP, VPN, ...

De manière générale on se trouve face à deux cas distincts :

  • Une seule passerelle internet est accessible via une interface réseau donnée.
  • Plusieurs passerelles internet sont accessibles via une même interface réseau donnée.

Pour que ce tutoriel fonctionne, on s'intéressera uniquement au premier cas. Autrement dit, on appliquera systématiquement la règle « 1 interface réseau = 1 passerelle = 1 accès internet », sachant que la notion d'interface réseau correspond à la vue fournie par la commande # ip address plutôt que # ifconfig (qui a le défaut d'afficher des doublons lorsqu'une interface dispose de plusieurs adresses IP).
Si une interface donnée permet l'accès à plusieurs passerelles, il faudra malheureusement faire un choix pour n'en utiliser qu'une seule.


On utilisera la combinaison d'outils suivante :

  • Tables de routage (iproute2) pour router les connexions vers l'une ou l'autre passerelle.
  • Marquage de paquets (iptables -j MARK) pour identifier quelle table de routage sera utilisée.
  • Marquage de connexions (iptables -j CONNMARK) pour déterminer les stratégies de routage et gérer l'affinité d'une connexion avec un accès donné.
  • Translation NAT (iptables -j SNAT) pour assurer le transit correct des paquets re-routés.


Note : un récapitulatif est disponible à la fin de ce document, qui résume toutes les commandes nécessaires à la création des diverses règles. Il n'est donc pas utile (et même contre-indiqué) de saisir les commandes au fil du tutoriel. Que cela ne vous empêche pas de lire les explications, leur compréhension est nécessaire pour vous éviter de faire des bêtises !

Sachez que si vous vous retrouvez sans connexion internet (ce qui peut arriver quand on manipule n'importe comment les tables de routage) et sans savoir comment réparer la situation, un simple reboot de votre machine suffira à rétablir votre configuration d'origine.


Obtenir de l'aide

Ce tutoriel était à l'origine un T&A sur le forum debian-fr.org. Étant donné l'ampleur que ce document a pris, il m'est difficile de maintenir une version équivalente sur le forum en ne disposant que de BBCode.

Cependant, le fil de discussion associé est toujours d'actualité pour la partie support. N'hésitez pas à y poser vos questions ou à partager vos trucs et astuces !

Avertissement

Le sujet traité serait assez complexe si l'on voulait être complet, je me suis donc permis de prendre des raccourcis au niveau des explications pour simplifier certains concepts. Si vous souhaitez apporter des précisions concernant certains points traités un peu trop vite et/ou de manière peu précise, demandez-vous simplement auparavant s'il ne s'agit pas d'une vulgarisation ou généralisation, et si un utilisateur lambda a réellement besoin de faire la distinction aussi finement. Ça évitera les discussions sans fin alors que nous sommes probablement d'accord... Mais que ça ne vous empêche pas de poser des questions si quelque chose ne vous paraît pas clair !


Configuration réseau utilisée

On supposera disposer de la configuration réseau suivante :

  • eth0 : adresse 192.168.1.100/24, passerelle 192.168.1.1, correspondant à l'accès internet par défaut utilisé actuellement.
  • eth1 : adresse 192.168.2.100/24, passerelle 192.168.2.1, correspondant à un accès internet non utilisé actuellement.

Il est bien évident que la technique présentée ici peut gérer sans problème plus de deux accès internet, il suffit d'adapter la configuration et les diverses règles en conséquence.


Problématiques rencontrées

Stratégies de routage

Le but de ce tutoriel étant de router arbitrairement des connexions via l'accès internet de notre choix, il est bien évident qu'il faudra porter une attention particulière aux règles que l'on choisira pour décider quelle connexion transitera par quel accès.

Les possibilités sont infinies, en voici quelques exemples :

  • Connexions e-mail (ports TCP 25 et 110) vers une connexion secondaire lente.
  • Trafic web (ports TCP 80 et 443) vers un FAI différent pour pouvoir profiter des offres réservées à ses abonnés.
  • Forcer les communications avec un serveur particulier à utiliser un accès sécurisé.
  • Faire transiter toutes les connexions d'un programme donné via un accès spécifique.
  • Etc...

On peut bien sûr avoir plusieurs stratégies actives en parallèle, encore une fois il s'agit simplement d'adapter les règles.


Au cours de ce tutoriel, nous nous intéresserons à une stratégie très simple : router le trafic SSH (à destination du port TCP 22) vers l'accès internet fourni par la passerelle 192.168.2.1 sur eth1.
Mais ne vous inquiétez pas, le récapitulatif final contient plusieurs autres « recettes » qui vous seront probablement plus utiles que ça.


Affinité des connexions

Un problème majeur est de savoir comment gérer les connexions lorsqu'un des accès internet devient indisponible.

En effet, on ne peut pas changer en cours de route l'accès internet utilisé par une connexion existante : la machine distante avec laquelle on discute ne reconnaîtrait pas les paquets comme appartenant à la connexion, et les ignorerait probablement sans nous répondre. Le résultat final serait que le programme émetteur, sur notre machine, se retrouverait bloqué jusqu'à ce que tous les timeouts prennent effet, ce qui peut être assez long (de l'ordre de plusieurs minutes voire dizaines de minutes).

Le problème réel est que lorsqu'une interface réseau devient indisponible (par exemple une connexion point à point fermée par le pair), toutes les entrées correspondantes sont supprimées des tables de routage, ce qui a normalement pour effet de re-router automatiquement les paquets vers un autre accès internet sans que nous puissions contrôler ce qui se passe.


La solution à ce problème particulier est de gérer la notion d'affinité d'une connexion réseau donnée avec un certain accès internet : lorsqu'une connexion a commencé à utiliser un accès, elle continuera de l'utiliser quels que soient les changements de configuration qui interviennent. Et si l'accès devient indisponible, les connexions existantes qui l'utilisent seront réinitialisées.

Concernant les nouvelles connexions censées utiliser l'accès indisponible, on se retrouve face à plusieurs choix :

  • Les autoriser à utiliser l'accès internet par défaut.
  • Les forcer à utiliser un autre accès internet spécifique.
  • Leur interdire d'utiliser un autre accès internet que celui prévu.

Pour des raisons de simplicité nous ne nous intéresserons ici qu'à la troisième option, qui a l'avantage d'homogénéiser le comportement des passerelles « classiques » (disponibles sur un LAN via une interface fiable de type Ethernet ou Wifi, mais n'offrant pas de garantie que l'accès internet soit fonctionnel) et celui des connexions point à point (dont la présence de l'interface virtuelle est dépendante de la disponibilité de l'accès internet).


Cela signifie donc qu'une connexion prévue pour utiliser un accès donné sera tout simplement refusée si cet accès est indisponible.

Encore une fois, comme précisé dans l'introduction, il est relativement aisé de rajouter par la suite des scripts de gestion d'indisponibilité (fail-over) mais nous n'entrerons pas ici dans les détails pour ne pas surcharger ce tutoriel.


Détermination des diverses constantes

Pour chacun des accès internet utilisés, nous devrons choisir plusieurs valeurs numériques :

  • Une valeur utilisée pour le marquage des paquets dans iptables (entre 1 et 4294967295) qui servira entre autres à nous aiguiller vers la bonne passerelle (via une table de routage spécifique).
  • Une valeur utilisée pour le marquage des connexions dans iptables (entre 1 et 4294967295) qui servira à gérer l'affinité d'une connexion avec un accès internet donné et, le cas échéant, à détecter automatiquement si une connexion doit être réinitialisée suite à l'indisponibilité de l'interface qu'elle utilise.
  • Une valeur utilisée pour identifier la table de routage permettant d'accéder à la passerelle internet (entre 1 et 2147483647 sauf 253, 254 et 255). Attention : les valeurs supérieures ou égales à 256 ne peuvent pas être supprimées par la commande # ip rule del table mais doivent être supprimées par # ip rule del priority ce qui rend l'automatisation de la tâche beaucoup plus délicate (pas impossible cependant si on est suffisamment têtu). Pour des raisons de simplicité, nous nous contenterons donc de valeurs comprises entre 0 et 252.


Nous devrons également choisir une valeur pour le marquage des connexions dans iptables, utilisée spécifiquement pour indiquer qu'une connexion doit être réinitialisée (cf. Affinité des connexions).


Afin de simplifier les choses, pour un même accès internet nous utiliserons une seule valeur identique pour le marquage de paquets, le marquage de connexions, et l'identification des tables de routage. Cela limite donc notre choix aux valeurs possibles pour les tables de routage.

On doit en outre s'assurer que les valeurs choisies ne rentrent pas en conflit les unes avec les autres, ni avec une configuration de marquage ou de routage pré-existante. Si vous ne savez si vous utilisez déjà du marquage ou des tables de routage spécifiques, c'est que ce n'est probablement pas le cas.


Valeurs utilisées dans ce tutoriel :

  • Marquage de connexions pour réinitialisation : 4321.
  • Marquage de paquets / connexions et tables de routage pour eth0 : 50.
  • Marquage de paquets / connexions et tables de routage pour eth1 : 51.


Tables de routage

Liens de routage

Pour chaque accès internet, il nous faut tout d'abord créer un lien entre le marquage des paquets et les tables de routage que nous allons créer juste après :

# ip rule add fwmark 50 table 50
# ip rule add fwmark 51 table 51


Création des tables de routage spécifiques

Par défaut, notre machine dispose d'une table de routage principale (la 254, nommée main) qui comporte toutes les informations nécessaires pour utiliser les réseaux locaux ainsi que l'accès internet par défaut (eth0 dans notre cas).

Pour chacun des accès internet on va recopier cette table en remplaçant la passerelle par défaut par la passerelle correspondant à l'accès en question :

#!/bin/sh

copyRoutingTable()
{
  local TABLE="$1"
  local IFACE="$2"
  local GATEWAY="$3"
  ip route flush table $TABLE
  ip route show table main \
    | grep -Ev ^default \
    | while read ROUTE ;
    do
      ip route add table $TABLE $ROUTE
    done
  ip route add table $TABLE default via $GATEWAY dev $IFACE
}

copyRoutingTable 50 eth0 192.168.1.1
copyRoutingTable 51 eth1 192.168.2.1
ip route flush cache

Note : cette opération de recopie devra avoir lieu à chaque fois que la table de routage principale sera modifiée, ce qui inclut les changements d'état des diverses interfaces. Encore une fois, les évènements up et down des interfaces concernées sont vos amis ! En pratique, lorsqu'une interface devient indisponible ses informations de routage sont tout bonnement supprimées de toutes les tables de routage, il n'y a donc besoin de recopier les tables que lorsqu'une interface (ré)apparaît (évènements up). Mais que cela ne vous empêche pas de changer vos stratégies de routage quand une interface disparaît...


À noter que la duplication de la table de routage pour l'accès par défaut permettra par la suite de définir des stratégies figées (telles connexions forcées sur tel ou tel accès internet) tout en laissant la possibilité, à l'aide d'une simple série de commandes, de modifier l'accès internet par défaut pour les connexions non marquées :

# ip route del table main default
# ip route add table main default via {IP de la nouvelle passerelle par défaut} dev {nom de la nouvelle interface par défaut}
# ip route flush cache


Règles iptables

Les règles iptables nécessaires au bon fonctionnement du routage sont un peu difficiles à suivre, nous allons donc faire une petite pause dans les commandes et examiner la chronologie de la vie d'un paquet à l'intérieur d'iptables :

  • Table mangle / OUTPUT : la toute première étape est de marquer les nouvelles connexions (state NEW) en fonction de nos stratégies de routage. Sans ce marquage aucune de nos autres règles ne sera utilisée, autrement dit la connexion se comportera comme si nous n'avions rien fait. C'est d'ailleurs pourquoi nous ne mettrons ce marquage en place qu'à la toute fin, lorsque le reste sera prêt à fonctionner.
  • Table mangle / OUTPUT : immédiatement après, pour tous les paquets, le marquage de la connexion est recopié dans le marquage du paquet afin de pouvoir sélectionner la bonne table de routage (grâce à la règle fwmark dans les liens de routage).
  • Table filter / OUTPUT : si la connexion est marquée avec la valeur spéciale de réinitialisation (ici, 4321) alors on agit en conséquence. À noter que, pour le moment, nous n'avons pas encore créé de règle pour marquer la connexion de cette manière, ce qui implique quelques subtilités dont nous reparlerons d'ici un instant (Suivi de l'affinité des connexions).
  • Une décision de re-routage est effectuée par le kernel en fonction du marquage de paquet. À partir d'ici, iptables est capable de reconnaître l'interface sur laquelle sera réellement envoyé le paquet, alors qu'auparavant il ne voyait que l'interface sur laquelle le paquet aurait dû être envoyé si nous n'étions pas intervenus.
  • Table mangle / POSTROUTING : maintenant que nous savons quelle interface le paquet va réellement utiliser, nous pouvons déterminer s'il s'agit bien de l'interface que nous avions prévue. Si ça n'est pas le cas, la connexion est marquée pour une réinitialisation ultérieure avec la valeur spéciale choisie (ici, 4321). Encore une fois, voir Suivi de l'affinité des connexions ci-après pour plus de détails.
  • Table nat / POSTROUTING : cette règle n'est appliquée qu'une seule fois en début de connexion, et a pour but de corriger l'adresse source de la connexion lorsque celle-ci a été re-routée vers une interface différente de celle qu'elle aurait dû utiliser initialement.


Dans l'absolu, nous pourrions nous contenter de créer les règles iptables une fois pour toutes dans le bon ordre. Cependant, en pratique il faudra probablement coupler ce tutoriel de routage avec des scripts de fail-over qui manipuleront les diverses règles en fonction de l'état des diverses interfaces. Pour simplifier la gestion dynamique de ces règles, nous allons donc créer une fois pour toutes des chaînes iptables nous permettant de catégoriser les règles et de garantir un ordonnancement correct (ce qui est particulièrement important dans la table mangle / OUTPUT) :

# iptables -t mangle -N ROUTING-POLICIES
# iptables -t mangle -N MARK-PACKETS
# iptables -t mangle -A OUTPUT -m state --state NEW -j ROUTING-POLICY
# iptables -t mangle -A OUTPUT -j MARK-PACKETS

# iptables -t filter -N CONNECTION-REINIT
# iptables -t mangle -N DETECT-CONNECTION-REINIT
# iptables -t filter -A OUTPUT -m connmark --mark 4321 -j CONNECTION-REINIT
# iptables -t mangle -A POSTROUTING -j DETECT-CONNECTION-REINIT

# iptables -t nat -N REROUTE-SNAT
# iptables -t nat -A POSTROUTING -j REROUTE-SNAT


Suivi de l'affinité des connexions

Les règles iptables en elles-mêmes sont assez simples. Pour la réinitialisation des connexions, on distinguera les connexions TCP (réinitialisées avec un paquet RST qui est la méthode la plus efficace) des autres types de connexions (qui recevront uniquement un message ICMP network unreachable) :

# iptables -t filter -A CONNECTION-REINIT -p tcp -j REJECT --reject-with tcp-reset
# iptables -t filter -A CONNECTION-REINIT        -j REJECT --reject-with icmp-net-unreachable

Pour détecter si une connexion doit être réinitialisée, il suffit de voir si l'interface réellement utilisée correspond ou non au marquage de la connexion. Si ça ne correspond pas, on marque pour réinitialisation ultérieure :

# iptables -t mangle -A DETECT-CONNECTION-REINIT -m connmark --mark 50 ! -o eth0 -j CONNMARK --set-mark 4321
# iptables -t mangle -A DETECT-CONNECTION-REINIT -m connmark --mark 51 ! -o eth1 -j CONNMARK --set-mark 4321


Un détail important à savoir est que, dans la vie d'un paquet, le test permettant de déterminer si une connexion doit être réinitialisée ne peut se faire qu'à un moment où le filtrage du paquet ne peut plus être réalisé. En pratique, ça implique qu'il y aura toujours un paquet qui « passera au travers », la réinitialisation proprement dite de la connexion n'ayant lieu qu'à partir du paquet suivant.


Translation NAT

Pour décider si une connexion a besoin d'être NATée, on vérifie d'abord que l'interface utilisée corresponde bien au marquage de la connexion, puis on n'effectue le NAT que si l'adresse source du paquet ne correspond pas à celle de l'interface utilisée :

# iptables -t nat -A REROUTE-SNAT -m connmark --mark 50 -o eth0 ! -s 192.168.1.100 -j SNAT --to-source 192.168.1.100
# iptables -t nat -A REROUTE-SNAT -m connmark --mark 51 -o eth1 ! -s 192.168.2.100 -j SNAT --to-source 192.168.2.100


Marquage des paquets à partir du marquage de connexion

Pas grand chose à dire, on ne fait que recopier le marquage de connexion à l'identique dans le paquet :

# iptables -t mangle -A MARK-PACKETS -m connmark --mark 50 -j MARK --set-mark 50
# iptables -t mangle -A MARK-PACKETS -m connmark --mark 51 -j MARK --set-mark 51


Marquage des connexions en fonction des stratégies de routage

On arrive (enfin !) au but du tutoriel : choisir quelles connexions vont être routées vers quel accès internet.

En l'occurrence nous allons, comme prévu dans Stratégies de routage, router toutes les connexions SSH (à destination du port TCP 22) vers l'accès internet secondaire (192.168.2.1 / eth1) :

# iptables -t mangle -A ROUTING-POLICY -p tcp --dport 22 -j CONNMARK --set-mark 51

Et voilà !


Récapitulatif

[TODO] Ça va venir bientôt, laissez moi respirer... ;-)



Utilisateur:Syam Expérimenté Wifi