resolve ip

This commit is contained in:
Mateusz Gruszczyński
2026-05-20 21:59:25 +02:00
parent f4d8611240
commit af20e55539
11 changed files with 364 additions and 159 deletions

View File

@@ -0,0 +1,99 @@
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