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:
parent
81b5a0fae2
commit
55d6316dda
3 changed files with 75 additions and 8 deletions
|
|
@ -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é
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
1.9.0
|
||||
1.9.1
|
||||
|
|
|
|||
71
app/app.py
71
app/app.py
|
|
@ -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,9 +840,30 @@ 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:]:
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in a new issue