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.
- Lirecompteur vnStat
- Comparerquota et état
- Agircouper ou rétablir
- Vérifierrelire l’effet
- 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.
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.
| Forme | NIC mesurée localement | Coupure systemctl | Isolation réelle | Quand la choisir |
|---|---|---|---|---|
| Unité systemd | oui | oui | sans objet | Le garde et l’interface facturée sont sur la même machine. |
| Docker, réseau hôte | oui, avec NET_ADMIN | non | quasi nulle | Parc déjà entièrement conteneurisé, et rien d’autre. |
| Docker, profil distant | non | non | complète | Le 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 :
| Code | Signification | Relancé | Pourquoi |
|---|---|---|---|
0 | Arrêt demandé | sans objet | Fin normale. |
2 | Configuration invalide | non | Une boucle de redémarrage masquerait la faute sans jamais garder le quota. |
3 | Verrou déjà détenu | non | Une autre instance surveille déjà : la seconde n’a rien à faire. |
4 | Échec inattendu du travailleur | oui | Une 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é
- Le compteur mensuel est délégué à vnStat : aucune remise à zéro à écrire, donc aucune à manquer.
- L’état est comparé à chaque cycle, jamais déclenché sur une date — la machine peut être éteinte au changement de mois.
- Une coupure n’est annoncée qu’après avoir été relue, et l’alerte transporte ce qui a été lu.
- 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.
- 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.