Stéphane.
LinuxTerminé

egress-guard — quota de trafic sortant sous Linux

Démon Python sans dépendance qui surveille le compteur mensuel d'une interface, coupe le trafic au dépassement du quota et le rétablit au changement de mois, avec vérification après coupure.

Technologies employées

  • Python
  • vnStat
  • systemd
  • Docker
  • Compose
  • SSH
  • unittest
  • ruff
  • GitLab CI

Résumé

Un hôte facturé au volume dispose d’une allocation mensuelle de trafic. Ce programme lit le compteur d’octets de l’interface concernée et, dès que le quota est franchi, exécute une commande de coupure choisie par l’exploitant ; il en exécute une autre lorsque le mois bascule. Un seul fichier, bibliothèque standard uniquement : ni installation de paquets, ni jq, ni curl.

Contexte

Le dépassement d’un forfait de trafic ne se manifeste jamais au moment où il se produit : il apparaît sur la facture, ou par une limitation imposée par le fournisseur. Les gardes-fous écrits à la va-vite partagent trois défauts. Ils supposent qu’une commande lancée est une commande réussie. Ils remettent le compteur à zéro eux-mêmes, avec de l’arithmétique de dates et une tâche planifiée à minuit qui peut être manquée. Et ils considèrent qu’un compteur illisible signifie « rien à signaler ».

Objectifs

  • Couper une liaison au franchissement du quota, sans supposer ce qu’est la coupure : arrêt d’interface, arrêt de service, règle de filtrage, alerte.
  • Rétablir automatiquement au changement de mois, y compris si la machine était éteinte au moment de la bascule.
  • Ne déclarer une coupure effective qu’après l’avoir relue.
  • Fonctionner sur un hôte quelconque, sans rien installer.

Contraintes

  • Python 3.9 comme plancher, imposé par os.waitstatus_to_exitcode() : c’est la version présente sur les distributions stables les plus anciennes visées.
  • Aucune dépendance externe hors vnstat, qui fournit le compteur.
  • Le compteur peut se trouver sur une autre machine, atteignable en SSH.
  • Un exploitant doit pouvoir essayer la configuration sur un hôte vivant sans rien couper.

Architecture

Le comptage est délégué à vnStat, qui ouvre une nouvelle ligne le premier jour du mois. Il n’y a donc volontairement aucune remise à zéro : au cycle suivant la bascule, le garde lit un compteur redevenu inférieur au quota, trouve un état « coupé » et exécute la commande de rétablissement. L’état est comparé, jamais utilisé comme déclencheur — ce qui supprime tout calcul de date et rend la bascule insensible à un arrêt de la machine.

Un processus superviseur détache la boucle de scrutation et la relance si elle meurt. Une erreur de configuration, elle, sort en code 2 et n’est délibérément pas relancée : une boucle de redémarrage sans garde actif est pire qu’un service arrêté. Un verrou flock interdit deux instances simultanées, et l’interrogation d’état demande au noyau qui détient ce verrou plutôt que de faire confiance au PID enregistré — un PID recyclé ne peut pas simuler un garde en marche.

  1. Lirecompteur vnStat
  2. Comparerquota et état
  3. Agircouper ou rétablir
  4. Vérifierrelire l’effet
  5. Alerteravec le résultat lu

Aucune étape n’est déclenchée par une date : c’est la comparaison entre le compteur et l’état enregistré qui décide, à chaque cycle.

Un cycle de scrutation. L’étape de vérification est celle qui distingue « la commande est passée » de « la liaison est effectivement coupée » : sans elle, l’alerte rapporterait une intention, pas un fait.

L’unité systemd est le déploiement principal : le programme mesure et coupe le trafic d’un hôte, c’est-à-dire précisément ce dont un conteneur l’isolerait. Docker garde sa place dans la forme « compteur distant », où le garde lit le compteur en SSH et n’a alors besoin d’aucun privilège.

FormeNIC mesurée localementCoupure systemctlIsolation réelleQuand la choisir
Unité systemdouiouisans objetLe garde et l’interface facturée sont sur la même machine.
Docker, réseau hôteoui, avec NET_ADMINnonquasi nulleParc déjà entièrement conteneurisé, et rien d’autre.
Docker, profil distantnonnoncomplèteLe compteur est ailleurs, lu en SSH. Aucun privilège requis.

Implémentation

Chaque option lit aussi sa variable d’environnement en majuscules, la ligne de commande l’emportant ; le même fichier sert donc à docker compose et à EnvironmentFile= de systemd. Les commandes de coupure, de rétablissement et de vérification reçoivent la situation dans leur environnement — instance, motif, volume, quota, pourcentage, sens et interface — et tournent sous /bin/sh -c, ce qui laisse écrire des enchaînements.

Trois comportements portent l’essentiel de la valeur :

La vérification après coupure s’exécute une fois la commande passée, et sa sortie standard est reprise dans l’alerte. « J’ai vérifié, l’interface est DOWN » vaut infiniment plus que « j’ai lancé la commande », et c’est ce qui rattrape une coupure qui n’a pas pris.

Le repli fermé distingue « sous le quota » de « aveugle ». Si le compteur reste illisible un nombre configuré de cycles consécutifs, le garde coupe quand même : ne plus voir le compteur n’est pas une raison de continuer à le dépenser. Une erreur passagère ne suffit pas, six d’affilée oui.

La réaffirmation au démarrage relance la coupure une fois à la reprise, un redémarrage suivant souvent un redémarrage de la machine — lequel annule indifféremment wg-quick, tc et iptables. Le mode par cycle existe, mais n’est légitime que pour une commande idempotente, ce que iptables -A n’est pas.

Deux canaux d’alerte, Telegram et Nextcloud Talk, indépendants et facultatifs. Une alerte perdue n’entraîne jamais le garde avec elle, mais un canal qui rejette une alerte est signalé plutôt que compté comme délivré : un 401 HTTP ressemble exactement à un succès pour une vérification naïve.

Difficultés rencontrées

Le mode d’essai devait pouvoir tourner sur un hôte de production sans effet de bord : ni commande exécutée, ni alerte envoyée, ni fichier d’état écrit. La frontière est facile à percer sans le vouloir, l’écriture d’état étant le chemin le plus discret.

Le second piège concerne la restitution : une invocation nue, sans rien de configuré, ne pouvait pas se contenter d’afficher l’aide et de sortir en 0. systemd comme Docker liraient ce 0 comme une exécution propre, laissant le quota sans surveillance.

Enfin, un garde silencieux est indiscernable d’un garde mort. Tant qu’une coupure est en vigueur, l’absence de nouvelles ne prouve rien.

Solutions

Les codes de sortie sont devenus contractuels — c’est par eux que systemd et Docker décident s’il faut relancer, et une erreur de configuration relancée en boucle laisse le quota sans garde :

CodeSignificationRelancéPourquoi
0Arrêt demandésans objetFin normale.
2Configuration invalidenonUne boucle de redémarrage masquerait la faute sans jamais garder le quota.
3Verrou déjà détenunonUne autre instance surveille déjà : la seconde n’a rien à faire.
4Échec inattendu du travailleurouiUne panne passagère ne doit pas laisser la liaison sans surveillance.

Une invocation nue affiche les exemples et sort en 2 : afficher l’aide n’est pas faire le travail, et un 0 se lirait comme une exécution propre.

Une horodatation est écrite à chaque cycle. La sonde de santé du conteneur la relit : c’est la différence entre « au-dessus du quota » (sain) et « la boucle ne tourne plus » (pas sain), deux situations qu’un simple processus vivant ne distingue pas. Un rappel quotidien est émis tant que la coupure dure — c’est son absence qui devient le signal.

La suite de tests fait passer les commandes de l’exploitant par un vrai /bin/sh et prend un vrai flock, parce que ce sont exactement les parties qui se comportent autrement dans un bouchon qu’à 100 % de l’allocation.

Résultats

  • 91 tests couvrant l’analyse des unités, la validation de configuration, le traitement du JSON de vnStat, les échecs d’envoi d’alerte et chaque transition d’état : coupure, rétablissement, repli fermé, surveillance seule, reprise et bascule de mois au travers d’un redémarrage.
  • Intégration continue sur les deux interpréteurs extrêmes supportés, plus une exécution sur Debian nue qui vérifie la promesse d’absence de dépendance : un module oublié ou un import égaré y ressort immédiatement.
  • L’image publiée est démarrée avant d’être poussée. Elle est inutile si le garde ne s’y lance pas, et la vérification coûte une seconde.
  • Une documentation d’exploitation qui associe à chaque alerte le premier geste, en désignant les trois seules qui justifient d’être réveillé la nuit.

En résumé

  1. Le compteur mensuel est délégué à vnStat : aucune remise à zéro à écrire, donc aucune à manquer.
  2. L’état est comparé à chaque cycle, jamais déclenché sur une date — la machine peut être éteinte au changement de mois.
  3. Une coupure n’est annoncée qu’après avoir été relue, et l’alerte transporte ce qui a été lu.
  4. Un compteur illisible six fois de suite vaut dépassement : le garde coupe plutôt que de dépenser une allocation qu’il ne voit plus.
  5. Une configuration invalide arrête le service ; seule une panne passagère est relancée.

Améliorations possibles

  • Export des métriques vers la supervision, plutôt qu’un état à interroger.
  • Prise en charge d’un compteur fourni directement par le noyau, pour les hôtes où vnStat ne peut pas être installé.
  • Fenêtre de quota glissante, pour les fournisseurs qui ne facturent pas au mois calendaire.