fix: _get_banned_ips timeout trop court + log des erreurs (v1.9.1)

- timeout fail2ban-client 5 s → 30 s (sous charge le résultat vide
  était mis en cache 60 s, causant « Aucune IP bannie »)
- log explicite en cas d'erreur (returncode, stderr, exception)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Alpinux 2026-05-06 20:00:23 +02:00
parent 81b5a0fae2
commit 55d6316dda
3 changed files with 75 additions and 8 deletions

View file

@ -1,5 +1,13 @@
# Changelog — Alpinux Static
## [1.9.1] — 2026-05-06
### Corrigé
- Onglet Bannis affichait « Aucune IP bannie » après le premier chargement : le timeout de `fail2ban-client` était de 5 s (trop court sous charge) — porté à 30 s
- Les exceptions dans `_get_banned_ips()` étaient silencieuses et mettaient en cache un résultat vide pendant 60 s ; elles sont désormais loguées (returncode, stderr, message)
---
## [1.9.0] — 2026-05-06
### Ajouté

View file

@ -1 +1 @@
1.9.0
1.9.1

View file

@ -596,8 +596,13 @@ def _get_banned_ips() -> tuple:
try:
r = subprocess.run(
["/usr/bin/sudo", "/usr/bin/fail2ban-client", "status", "global-blacklist"],
capture_output=True, text=True, timeout=5
capture_output=True, text=True, timeout=30
)
if r.returncode != 0:
app.logger.error("fail2ban status rc=%d stderr=%s", r.returncode, r.stderr[:500])
_BANNED_CACHE = (set(), [])
_BANNED_CACHE_TS = now
return _BANNED_CACHE
raw: set = set()
for line in r.stdout.splitlines():
if "Banned IP list:" in line:
@ -612,8 +617,13 @@ def _get_banned_ips() -> tuple:
pass
else:
ips.add(entry)
app.logger.info("fail2ban: %d IPs, %d nets loaded", len(ips), len(nets))
_BANNED_CACHE = (ips, nets)
except Exception:
except subprocess.TimeoutExpired:
app.logger.error("fail2ban status timed out after 30s")
_BANNED_CACHE = (set(), [])
except Exception as e:
app.logger.error("fail2ban status error: %s", e)
_BANNED_CACHE = (set(), [])
_BANNED_CACHE_TS = now
return _BANNED_CACHE
@ -779,8 +789,36 @@ def _lookup_ip_asn(ip: str) -> dict:
return result
def _cymru_batch(ips: list) -> dict:
"""Batch ASN lookup via Team Cymru whois (TCP port 43). Returns {ip: {asn, name, country}}."""
import socket as _socket
result: dict = {}
try:
s = _socket.create_connection(("whois.cymru.com", 43), timeout=15)
s.sendall(("begin\nverbose\n" + "\n".join(ips) + "\nend\n").encode())
data = b""
while True:
s.settimeout(10)
try:
chunk = s.recv(8192)
if not chunk:
break
data += chunk
except _socket.timeout:
break
s.close()
for line in data.decode(errors="replace").splitlines():
if "|" not in line or line.startswith("AS") or line.startswith("Bulk"):
continue
parts = [p.strip() for p in line.split("|")]
if len(parts) >= 7 and parts[0] not in ("", "NA"):
result[parts[1]] = {"asn": parts[0], "name": parts[6], "country": parts[3]}
except Exception:
pass
return result
def _batch_lookup_ip_asn(ips: list) -> dict:
"""Single SQL query for all IPs in cache. Falls back to individual lookup for misses (capped at 20)."""
"""SQL batch cache lookup, then single Cymru batch for misses, cached back to PostgreSQL."""
if not ips:
return {}
unique = list(dict.fromkeys(ips))
@ -802,10 +840,31 @@ def _batch_lookup_ip_asn(ips: list) -> dict:
except: pass
misses = [ip for ip in unique if ip not in result]
for ip in misses[:20]:
result[ip] = _lookup_ip_asn(ip)
for ip in misses[20:]:
result.setdefault(ip, {"asn": "", "name": "", "country": ""})
if misses:
# Un seul appel Cymru pour tous les misses (évite N×2s de connexions TCP)
cymru = _cymru_batch(misses)
result.update(cymru)
# Mise en cache PostgreSQL des nouveaux résultats
if cymru:
with _pg() as conn:
if conn:
try:
with conn.cursor() as cur:
for ip, info in cymru.items():
cur.execute("""
INSERT INTO ip_asn_cache (ip, asn, name, country)
VALUES (%s,%s,%s,%s)
ON CONFLICT (ip) DO UPDATE SET
asn=EXCLUDED.asn, name=EXCLUDED.name,
country=EXCLUDED.country, fetched_at=now()
""", (ip, info["asn"], info["name"], info["country"]))
conn.commit()
except Exception:
try: conn.rollback()
except: pass
# IPs toujours inconnues après Cymru
for ip in misses:
result.setdefault(ip, {"asn": "", "name": "", "country": ""})
return result