mirror of
https://github.com/thatmattlove/hyperglass.git
synced 2026-05-03 13:16:26 +00:00
MAJOR ENHANCEMENTS: IP Enrichment Service (hyperglass/external/ip_enrichment.py): - Increase IXP data cache duration from 24 hours to 7 days (604800s) for better performance - Fix critical cache refresh logic: ensure_data_loaded() now properly checks expiry before using existing pickle files - Remove 'force' refresh parameters from public APIs and admin endpoints to prevent potential abuse/DDOS - Implement automatic refresh based on file timestamps and cache duration - Add comprehensive debug logging gated by Settings.debug throughout the module - Clean up verbose comments and improve code readability - Update configuration model to enforce 7-day minimum cache timeout MikroTik Traceroute Processing: - Refactor trace_route_mikrotik plugin to use garbage cleaner before structured parsing - Only log raw router output when Settings.debug is enabled to reduce log verbosity - Simplify MikrotikTracerouteTable parser to expect pre-cleaned input from garbage cleaner - Remove complex multi-table detection, format detection, and deduplication logic (handled by cleaner) - Add concise debug messages for processing decisions and configuration states Traceroute IP Enrichment (traceroute_ip_enrichment.py): - Implement concurrent reverse DNS lookups using asyncio.to_thread and asyncio.gather - Add async wrapper for reverse DNS with proper error handling and fallbacks - Significant performance improvement for multi-hop traceroutes (parallel vs sequential DNS) - Proper debug logging gates: only detailed logs when Settings.debug=True - Upgrade operational messages to log.info level (start/completion status) - Maintain compatibility with different event loop contexts and runtime environments Configuration Updates: - Update structured.ip_enrichment.cache_timeout default to 604800 seconds - Update documentation to reflect new cache defaults and behavior - Remove force refresh options from admin API endpoints MIGRATION NOTES: - Operators should ensure /etc/hyperglass/ip_enrichment directory is writable - Any code relying on force refresh parameters must be updated - Monitor logs for automatic refresh behavior and performance improvements - The 7-day cache significantly reduces PeeringDB API load PERFORMANCE BENEFITS: - Faster traceroute enrichment due to concurrent DNS lookups - Reduced external API calls with longer IXP cache duration - More reliable refresh logic prevents stale cache usage - Cleaner, more focused debug output when debug mode is disabled TECHNICAL DETAILS: - Uses asyncio.to_thread for non-blocking DNS operations - Implements process-wide file locking for safe concurrent cache updates - Robust fallbacks for various asyncio execution contexts - Maintains backward compatibility while improving performance FILES MODIFIED: - hyperglass/external/ip_enrichment.py - hyperglass/models/config/structured.py - hyperglass/api/routes.py - hyperglass/plugins/_builtin/trace_route_mikrotik.py - hyperglass/models/parsing/mikrotik.py - hyperglass/plugins/_builtin/traceroute_ip_enrichment.py - docs/pages/configuration/config/structured-output.mdx
210 lines
8.6 KiB
Python
210 lines
8.6 KiB
Python
"""IP enrichment for structured traceroute data."""
|
|
|
|
# Standard Library
|
|
import asyncio
|
|
import socket
|
|
import typing as t
|
|
|
|
# Third Party
|
|
from pydantic import PrivateAttr
|
|
|
|
# Project
|
|
from hyperglass.log import log
|
|
from hyperglass.plugins._output import OutputPlugin
|
|
from hyperglass.models.data.traceroute import TracerouteResult
|
|
|
|
if t.TYPE_CHECKING:
|
|
from hyperglass.models.data import OutputDataModel
|
|
from hyperglass.models.api.query import Query
|
|
|
|
|
|
class ZTracerouteIpEnrichment(OutputPlugin):
|
|
"""Enrich structured traceroute output with IP enrichment ASN/organization data and reverse DNS."""
|
|
|
|
_hyperglass_builtin: bool = PrivateAttr(True)
|
|
platforms: t.Sequence[str] = (
|
|
"mikrotik_routeros",
|
|
"mikrotik_switchos",
|
|
"mikrotik",
|
|
"cisco_ios",
|
|
"juniper_junos",
|
|
"huawei",
|
|
"huawei_vrpv8",
|
|
)
|
|
directives: t.Sequence[str] = ("traceroute", "MikroTik_Traceroute")
|
|
common: bool = True
|
|
|
|
def _reverse_dns_lookup(self, ip: str) -> t.Optional[str]:
|
|
"""Perform reverse DNS lookup for an IP address."""
|
|
from hyperglass.settings import Settings
|
|
|
|
try:
|
|
hostname = socket.gethostbyaddr(ip)[0]
|
|
if Settings.debug:
|
|
log.debug(f"Reverse DNS for {ip}: {hostname}")
|
|
return hostname
|
|
except (socket.herror, socket.gaierror, socket.timeout) as e:
|
|
if Settings.debug:
|
|
log.debug(f"Reverse DNS lookup failed for {ip}: {e}")
|
|
return None
|
|
|
|
async def _reverse_dns_lookup_async(self, ip: str) -> t.Optional[str]:
|
|
"""Async wrapper around synchronous reverse DNS lookup using a thread.
|
|
|
|
Uses asyncio.to_thread to avoid blocking the event loop and allows
|
|
multiple lookups to be scheduled concurrently.
|
|
"""
|
|
try:
|
|
return await asyncio.to_thread(self._reverse_dns_lookup, ip)
|
|
except Exception as e:
|
|
from hyperglass.settings import Settings
|
|
|
|
if Settings.debug:
|
|
log.debug(f"Reverse DNS async lookup error for {ip}: {e}")
|
|
return None
|
|
|
|
async def _enrich_async(self, output: TracerouteResult) -> None:
|
|
"""Async helper to enrich traceroute data.
|
|
|
|
This performs IP enrichment (ASN lookups), ASN organization lookups,
|
|
and then runs reverse DNS lookups concurrently for hops missing hostnames.
|
|
"""
|
|
# First enrich with IP information (ASN numbers)
|
|
await output.enrich_with_ip_enrichment()
|
|
|
|
# Then enrich ASN numbers with organization names
|
|
await output.enrich_asn_organizations()
|
|
|
|
# Concurrent reverse DNS for hops missing hostnames
|
|
ips_to_lookup: list[str] = [
|
|
hop.ip_address for hop in output.hops if hop.ip_address and hop.hostname is None
|
|
]
|
|
if not ips_to_lookup:
|
|
return
|
|
|
|
# Schedule lookups concurrently
|
|
tasks = [asyncio.create_task(self._reverse_dns_lookup_async(ip)) for ip in ips_to_lookup]
|
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
|
|
# Apply results back to hops in order
|
|
idx = 0
|
|
for hop in output.hops:
|
|
if hop.ip_address and hop.hostname is None:
|
|
res = results[idx]
|
|
idx += 1
|
|
if isinstance(res, Exception):
|
|
from hyperglass.settings import Settings
|
|
|
|
if Settings.debug:
|
|
log.debug(f"Reverse DNS lookup raised for {hop.ip_address}: {res}")
|
|
else:
|
|
hop.hostname = res
|
|
|
|
def process(self, *, output: "OutputDataModel", query: "Query") -> "OutputDataModel":
|
|
"""Enrich structured traceroute data with IP enrichment and reverse DNS information."""
|
|
|
|
if not isinstance(output, TracerouteResult):
|
|
return output
|
|
|
|
_log = log.bind(plugin=self.__class__.__name__)
|
|
|
|
# Import Settings for debug gating
|
|
from hyperglass.settings import Settings
|
|
|
|
_log.info(
|
|
f"Starting IP enrichment for {len(output.hops)} traceroute hops"
|
|
) # Check if IP enrichment is enabled in config
|
|
try:
|
|
from hyperglass.state import use_state
|
|
|
|
params = use_state("params")
|
|
# If structured config missing or traceroute enrichment disabled, skip
|
|
# IP enrichment but still perform reverse DNS lookups.
|
|
if (
|
|
not getattr(params, "structured", None)
|
|
or not params.structured.ip_enrichment.enrich_traceroute
|
|
or getattr(params.structured, "enable_for_traceroute", None) is False
|
|
):
|
|
if Settings.debug:
|
|
_log.debug("IP enrichment for traceroute disabled in configuration")
|
|
# Still do reverse DNS if enrichment is disabled
|
|
# Perform concurrent reverse DNS lookups for hops needing hostnames
|
|
ips = [
|
|
hop.ip_address for hop in output.hops if hop.ip_address and hop.hostname is None
|
|
]
|
|
if ips:
|
|
try:
|
|
# Run lookups in an event loop
|
|
try:
|
|
loop = asyncio.get_event_loop()
|
|
if loop.is_running():
|
|
# We're inside an event loop; run tasks via asyncio.run in thread
|
|
import concurrent.futures
|
|
|
|
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
future = executor.submit(
|
|
lambda: asyncio.run(
|
|
asyncio.gather(
|
|
*[self._reverse_dns_lookup_async(ip) for ip in ips]
|
|
)
|
|
)
|
|
)
|
|
results = future.result()
|
|
else:
|
|
results = loop.run_until_complete(
|
|
asyncio.gather(
|
|
*[self._reverse_dns_lookup_async(ip) for ip in ips]
|
|
)
|
|
)
|
|
except RuntimeError:
|
|
results = asyncio.run(
|
|
asyncio.gather(*[self._reverse_dns_lookup_async(ip) for ip in ips])
|
|
)
|
|
|
|
# Apply results
|
|
idx = 0
|
|
for hop in output.hops:
|
|
if hop.ip_address and hop.hostname is None:
|
|
res = results[idx]
|
|
idx += 1
|
|
if not isinstance(res, Exception):
|
|
hop.hostname = res
|
|
except Exception as e:
|
|
if Settings.debug:
|
|
_log.debug(
|
|
f"Concurrent reverse DNS failed (fallback to sequential): {e}"
|
|
)
|
|
for hop in output.hops:
|
|
if hop.ip_address and hop.hostname is None:
|
|
hop.hostname = self._reverse_dns_lookup(hop.ip_address)
|
|
return output
|
|
except Exception as e:
|
|
if Settings.debug:
|
|
_log.debug(f"Could not check IP enrichment config: {e}")
|
|
|
|
# Use the built-in enrichment method from TracerouteResult
|
|
try:
|
|
# Run async enrichment in sync context
|
|
loop = None
|
|
try:
|
|
loop = asyncio.get_event_loop()
|
|
if loop.is_running():
|
|
# If we're already in an event loop, create a new task
|
|
import concurrent.futures
|
|
|
|
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
future = executor.submit(asyncio.run, self._enrich_async(output))
|
|
future.result()
|
|
else:
|
|
loop.run_until_complete(self._enrich_async(output))
|
|
except RuntimeError:
|
|
# No event loop, create one
|
|
asyncio.run(self._enrich_async(output))
|
|
_log.info("IP enrichment completed successfully")
|
|
except Exception as e:
|
|
_log.error(f"IP enrichment failed: {e}")
|
|
|
|
# Reverse DNS lookups already handled in _enrich_async for missing hostnames
|
|
|
|
_log.info(f"Completed enrichment for traceroute to {output.target}")
|
|
return output
|