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
|
# 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
|
## [1.9.0] — 2026-05-06
|
||||||
|
|
||||||
### Ajouté
|
### 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:
|
try:
|
||||||
r = subprocess.run(
|
r = subprocess.run(
|
||||||
["/usr/bin/sudo", "/usr/bin/fail2ban-client", "status", "global-blacklist"],
|
["/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()
|
raw: set = set()
|
||||||
for line in r.stdout.splitlines():
|
for line in r.stdout.splitlines():
|
||||||
if "Banned IP list:" in line:
|
if "Banned IP list:" in line:
|
||||||
|
|
@ -612,8 +617,13 @@ def _get_banned_ips() -> tuple:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
ips.add(entry)
|
ips.add(entry)
|
||||||
|
app.logger.info("fail2ban: %d IPs, %d nets loaded", len(ips), len(nets))
|
||||||
_BANNED_CACHE = (ips, 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 = (set(), [])
|
||||||
_BANNED_CACHE_TS = now
|
_BANNED_CACHE_TS = now
|
||||||
return _BANNED_CACHE
|
return _BANNED_CACHE
|
||||||
|
|
@ -779,8 +789,36 @@ def _lookup_ip_asn(ip: str) -> dict:
|
||||||
|
|
||||||
return result
|
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:
|
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:
|
if not ips:
|
||||||
return {}
|
return {}
|
||||||
unique = list(dict.fromkeys(ips))
|
unique = list(dict.fromkeys(ips))
|
||||||
|
|
@ -802,9 +840,30 @@ def _batch_lookup_ip_asn(ips: list) -> dict:
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
misses = [ip for ip in unique if ip not in result]
|
misses = [ip for ip in unique if ip not in result]
|
||||||
for ip in misses[:20]:
|
if misses:
|
||||||
result[ip] = _lookup_ip_asn(ip)
|
# Un seul appel Cymru pour tous les misses (évite N×2s de connexions TCP)
|
||||||
for ip in misses[20:]:
|
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": ""})
|
result.setdefault(ip, {"asn": "", "name": "", "country": ""})
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue