#32 — Générateur nftables
PLANIFIÉ
Priorité: 🔴 CRITIQUE · Type: TYPE E · Conteneur: rgz-gateway · Code: scripts/gateway/nftables_generator.pyDépendances: #7 rgz-gateway
Description
Génération dynamique des règles nftables par revendeur et VLAN. Le générateur construit une politique deny-all par défaut, puis alloue des règles spécifiques pour chaque revendeur. Chaque revendeur obtient une table dédiée rgz_vlan_NNN isolant son trafic, avec SNAT vers la gateway, whitelist DNS/NTP et routage vers le portail captif pour l'authentification.
Le script Python interroge la base de données PostgreSQL, lit les allocations VLAN depuis la table reseller_sites, et génère les règles nftables correspondantes. Les règles sont appliquées atomiquement via subprocess.run(['nft', ...]) avec gestion des erreurs et fallback. Une surveillance continue (inotifywait ou polling) recharge les règles si la DB change, permettant l'ajout/suppression de revendeurs sans redémarrage du conteneur.
Conformément à LL#40 et LL#41, le script ne supprime JAMAIS le ruleset entier avec nft flush table. Au lieu de cela, il supprime les tables custom une par une via nft delete table inet rgz_X 2>/dev/null || true, puis les recrée. L'entrypoint.sh du conteneur nettoie les tables au démarrage pour éviter les orphelins.
Architecture Interne
1. Démarrage rgz-gateway:
└─> entrypoint.sh:
├─> nft delete table inet rgz_filter 2>/dev/null || true [nettoyage]
├─> nft delete table inet rgz_vlan_* 2>/dev/null || true [orphelins]
├─> python3 scripts/gateway/nftables_generator.py --init
│ ├─> SELECT reseller_sites + vlan_id
│ ├─> Générer nftables.nft complet
│ └─> nft -f nftables.nft
└─> python3 scripts/gateway/watch_config.sh [monitoring continu]
2. Génération une table régulatrice (rgz_filter):
├─> Basée sur deny-all FORWARD
├─> Chaînes:
│ ├─ ingress_vlan: VLAN → revendeur, DROP si VLAN unknown
│ ├─ egress_vlan: revendeur → sortie, SNAT vers gateway
│ ├─ whitelist: DNS 53/udp, NTP 123/udp, ICMP (ping)
│ └─ dns_sinkhole: redirect vers portail avant auth
└─> Policies:
├─ Trafic entre VLANs du même revendeur: ACCEPT (multi-site)
├─ Trafic entre VLANs différents: DROP (isolation)
├─ Sortie SNAT: sourc → 10.0.0.1 (IP gateway)
└─ Entrée inverse: ACCEPT si état ESTABLISHED
3. Génération une table par VLAN (rgz_vlan_NNN):
└─> Pour chaque reseller_site:
├─> table inet rgz_vlan_150
├─> chain forward_150
│ ├─ ACCEPT vers DNS (whitelist Unbound sinkhole)
│ ├─ ACCEPT vers NTP
│ ├─ DROP verso ext services si pas auth (avant captif)
│ └─ ACCEPT si direction ESTABLISHED
└─> Logging: log prefix "vlan-150:" group 5 (netlink)
4. Réactions aux changements DB:
└─> watch_config.sh (inotifywait ou polling 60s):
├─> Vérifier nouvelle table dans DB
├─> Si NOUVEAU: créer rgz_vlan_NNN, appliquer règles
├─> Si SUPPRESSION: nft delete table inet rgz_vlan_NNN
└─> Log: syslog → ELK (#40)
5. DBA recalcul (#26):
└─> CoA RADIUS → rgz-api → UPDATE rate_limit WHERE nas_id = ?
└─> watch_config.sh applique limit via tc (HTB + fq_codel en #27)Configuration
Variables d'environnement
DB_HOST=rgz-db # PostgreSQL hostname
DB_PORT=5432
DB_NAME=rgz_db
DB_USER=rgz_gateway # Service account (SELECT only)
DB_PASSWORD=... # From .env
GATEWAY_IP=10.0.0.1 # IP sortie SNAT
WATCH_INTERVAL=60 # Polling interval (secondes)
NFT_DRYRUN=false # true = test sans appliquer
NETLINK_GROUP=5 # Pour logging nft → netlinkStructure de génération
# scripts/gateway/nftables_generator.py
def generate_nftables() -> str:
"""Génère config nftables complète"""
rules = []
# 1. Table régulatrice
rules.append("""
table inet rgz_filter {
chain prerouting { type filter hook prerouting priority -300; policy accept; }
chain forward { type filter hook forward priority 0; policy drop; }
chain output { type filter hook output priority 0; policy accept; }
chain ingress_vlan
chain egress_vlan
chain whitelist
}
""")
# 2. Tables par VLAN
sites = db.session.query(ResellerSites).all()
for site in sites:
vlan_id = site.vlan_id
rules.append(f"""
table inet rgz_vlan_{vlan_id} {{
chain forward_{vlan_id} {{
type filter hook forward priority 0;
policy drop;
# Whitelist DNS
ip daddr 10.0.0.9 udp dport 53 accept
ip daddr 10.0.0.9 tcp dport 53 accept
# Whitelist NTP
udp dport 123 accept
# ESTABLISHED/RELATED
ct state established,related accept
# Logging
log prefix "vlan-{vlan_id}:" group {NETLINK_GROUP}
}}
}}
""")
return "\n".join(rules)
def apply_nftables(config: str) -> bool:
"""Applique config via nft"""
try:
result = subprocess.run(
["nft", "-f", "-"],
input=config.encode(),
capture_output=True,
timeout=10
)
if result.returncode == 0:
logger.info("nftables appliqué")
return True
else:
logger.error(f"nft error: {result.stderr.decode()}")
return False
except Exception as e:
logger.error(f"Exception applying nft: {e}")
return False
def cleanup_stale_tables():
"""Supprime tables orphelins"""
try:
result = subprocess.run(
["nft", "list", "tables"],
capture_output=True,
timeout=5
)
tables = result.stdout.decode().split("\n")
for table in tables:
if "rgz_vlan_" in table and not is_active(table):
subprocess.run(["nft", "delete", "table", "inet", table],
capture_output=True, timeout=5)
except Exception as e:
logger.warning(f"cleanup_stale_tables: {e}")Endpoints API (indirects)
| Méthode | Route | Réponse | Auth |
|---|---|---|---|
| GET | /api/v1/gateway/nftables/status | 200 {loaded, tables_count, last_reload} | Admin |
| POST | /api/v1/gateway/nftables/reload | 202 {status: queued} | Admin |
| GET | /api/v1/gateway/nftables/logs?vlan=150 | 200 {items: [...], total} | Admin |
(Les endpoints sont sur rgz-api, le générateur tourne dans rgz-gateway.)
Commandes Utiles
# Générer et appliquer configuration (manuel)
python3 /scripts/gateway/nftables_generator.py --apply
# Mode dry-run (affiche sans appliquer)
python3 /scripts/gateway/nftables_generator.py --dry-run
# Vérifier tables chargées
nft list tables
# Lister règles d'une table spécifique
nft list table inet rgz_vlan_150
# Démarrer watchdog (monitoring continu)
python3 /scripts/gateway/watch_config.sh
# Voir les logs netlink nftables
tcpdump -i nflog:5 -A # Group 5
# Supprimer une table (orphelin)
nft delete table inet rgz_vlan_150
# Vérifier la syntaxe nft avant appliquer
nft -c -f /tmp/nftables.nftImplémentation TODO
- [ ] Implémenter
generate_nftables()avec table rgz_filter + rgz_vlan_NNN - [ ] Connexion PostgreSQL dans Python (psycopg2, échappement SQL)
- [ ] Fonction
apply_nftables(config)viasubprocess.run(['nft', '-f', '-']) - [ ] Fonction
cleanup_stale_tables()pour suppression sélective (LL#40, LL#41) - [ ] Entrypoint.sh: nettoyage au démarrage avant appliquer
- [ ] Watchdog inotifywait ou polling DB toutes les 60s
- [ ] Logging → syslog → ELK (integration #40)
- [ ] Synchronisation avec #26 DBA et #27 HTB QoS
- [ ] Tests: création table, suppression, reload après changement DB
- [ ] Tests LL#40/LL#41: vérifier pas de nft flush, seulement nft delete table
- [ ] Intégration #31 VLAN: lecture reseller_sites, génération par vlan_id
- [ ] Isolement multi-site: même VLAN = trafic ACCEPT, VLAN différents = DROP
Dernière mise à jour: 2026-02-21