100 lines
3.0 KiB
Python
100 lines
3.0 KiB
Python
from __future__ import annotations
|
|
|
|
import ipaddress
|
|
import socket
|
|
import time
|
|
from concurrent.futures import ThreadPoolExecutor, wait
|
|
from threading import Lock
|
|
from typing import Any
|
|
|
|
_CACHE_TTL_SECONDS = 24 * 60 * 60
|
|
_NEGATIVE_TTL_SECONDS = 60 * 60
|
|
_CACHE_LIMIT = 2048
|
|
_LOOKUP_LIMIT_PER_REQUEST = 24
|
|
_LOOKUP_TIMEOUT_SECONDS = 0.8
|
|
|
|
_cache: dict[str, tuple[str, float]] = {}
|
|
_pending: dict[str, Any] = {}
|
|
_lock = Lock()
|
|
_executor = ThreadPoolExecutor(max_workers=3, thread_name_prefix="reverse-dns")
|
|
|
|
|
|
def _is_resolvable_ip(value: str) -> bool:
|
|
try:
|
|
ipaddress.ip_address(str(value or "").strip())
|
|
return True
|
|
except ValueError:
|
|
return False
|
|
|
|
|
|
def _lookup_host(ip: str) -> str:
|
|
try:
|
|
host = socket.gethostbyaddr(ip)[0]
|
|
return str(host or "").rstrip(".")
|
|
except Exception:
|
|
return ""
|
|
|
|
|
|
def _trim_cache(now: float) -> None:
|
|
expired = [ip for ip, (_, expires_at) in _cache.items() if expires_at <= now]
|
|
for ip in expired:
|
|
_cache.pop(ip, None)
|
|
if len(_cache) <= _CACHE_LIMIT:
|
|
return
|
|
for ip, _ in sorted(_cache.items(), key=lambda item: item[1][1])[: len(_cache) - _CACHE_LIMIT]:
|
|
_cache.pop(ip, None)
|
|
|
|
|
|
def _store(ip: str, host: str, now: float | None = None) -> None:
|
|
now = now or time.monotonic()
|
|
ttl = _CACHE_TTL_SECONDS if host else _NEGATIVE_TTL_SECONDS
|
|
_cache[ip] = (host, now + ttl)
|
|
|
|
|
|
def attach_reverse_dns(peers: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
"""Attach cached or newly resolved PTR hostnames to peer rows with a small request budget."""
|
|
now = time.monotonic()
|
|
missing: list[str] = []
|
|
with _lock:
|
|
_trim_cache(now)
|
|
for peer in peers:
|
|
ip = str(peer.get("ip") or "").strip()
|
|
if not ip or not _is_resolvable_ip(ip):
|
|
peer["host"] = ""
|
|
continue
|
|
cached = _cache.get(ip)
|
|
if cached and cached[1] > now:
|
|
peer["host"] = cached[0]
|
|
continue
|
|
peer["host"] = ""
|
|
if ip not in _pending and ip not in missing and len(missing) < _LOOKUP_LIMIT_PER_REQUEST:
|
|
missing.append(ip)
|
|
for ip in missing:
|
|
_pending[ip] = _executor.submit(_lookup_host, ip)
|
|
futures = list(_pending.items())
|
|
|
|
if futures:
|
|
wait([future for _, future in futures], timeout=_LOOKUP_TIMEOUT_SECONDS)
|
|
|
|
done_hosts: dict[str, str] = {}
|
|
with _lock:
|
|
now = time.monotonic()
|
|
for ip, future in list(_pending.items()):
|
|
if not future.done():
|
|
continue
|
|
try:
|
|
host = str(future.result() or "")
|
|
except Exception:
|
|
host = ""
|
|
_store(ip, host, now)
|
|
done_hosts[ip] = host
|
|
_pending.pop(ip, None)
|
|
|
|
for peer in peers:
|
|
ip = str(peer.get("ip") or "").strip()
|
|
if ip in done_hosts:
|
|
peer["host"] = done_hosts[ip]
|
|
elif not peer.get("host") and ip in _pending:
|
|
peer["host_pending"] = True
|
|
return peers
|