"""Trazado de la ruta de red hacia un host via el binario `traceroute` (Linux). Funcion IMPURA: ejecuta `traceroute -m -w 2 ` como subprocess y parsea best-effort cada hop: numero de salto, hosts (nombre + IP) y los rtt detectados. Un hop sin respuesta ("* * *") se representa con `hosts` vacio. No busca un parseo perfecto: captura el numero de hop y las IPs por regex. Nunca lanza; devuelve dict de estado con `raw` siempre presente. """ import re import subprocess # Inicio de una linea de hop: numero de salto al principio. _HOP_LINE_RE = re.compile(r"^\s*(\d+)\s+(.*)$") # IPv4 entre parentesis o suelta. _IP_RE = re.compile(r"\b(\d{1,3}(?:\.\d{1,3}){3})\b") # "nombre (ip)" o "ip" _HOST_PAREN_RE = re.compile(r"([\w.\-]+)\s+\((\d{1,3}(?:\.\d{1,3}){3})\)") # rtt en milisegundos, p.ej "1.234 ms" _RTT_RE = re.compile(r"([\d.]+)\s*ms") def _parse_hop(hop_num: int, rest: str) -> dict: """Parsea best-effort el cuerpo de una linea de hop.""" hosts: list[dict] = [] # Caso sin respuesta: solo asteriscos. if rest.replace("*", "").strip() == "": return {"hop": hop_num, "hosts": []} # rtt globales de la linea (se asocian al/los host(s) detectados). rtts = [float(x) for x in _RTT_RE.findall(rest)] # Hosts con formato "nombre (ip)". paren_matches = _HOST_PAREN_RE.findall(rest) seen_ips: set[str] = set() for name, ip in paren_matches: seen_ips.add(ip) hosts.append({"name": name, "ip": ip, "rtt_ms": rtts}) # IPs sueltas no capturadas por el patron "nombre (ip)". for ip in _IP_RE.findall(rest): if ip not in seen_ips: seen_ips.add(ip) hosts.append({"name": "", "ip": ip, "rtt_ms": rtts}) # Si no detectamos ningun host pero la linea tiene contenido (raro), # dejamos un host placeholder con el texto en name para no perder info. if not hosts and rest.strip() and rest.strip() != "*": hosts.append({"name": rest.strip(), "ip": "", "rtt_ms": rtts}) return {"hop": hop_num, "hosts": hosts} def traceroute_host(host: str, max_hops: int = 30, timeout_s: int = 60) -> dict: """Traza la ruta de red hacia un host y parsea los hops best-effort. Args: host: Hostname o IP destino (ej. ``"google.com"`` o ``"1.1.1.1"``). max_hops: Maximo numero de saltos a sondear (`traceroute -m`). timeout_s: Timeout duro del subprocess en segundos. Returns: Dict de estado. En exito:: { "status": "ok", "host": , "hops": [ {"hop": 1, "hosts": [{"name": str, "ip": str, "rtt_ms": [float, ...]}]}, {"hop": 2, "hosts": []}, # "* * *" sin respuesta ... ], "raw": , } En fallo (binario ausente, host vacio, timeout duro):: {"status": "error", "error": , "host": , "raw": } """ if not host or not host.strip(): return {"status": "error", "error": "traceroute_host: host vacio", "host": host, "raw": ""} host = host.strip() try: proc = subprocess.run( ["traceroute", "-m", str(max_hops), "-w", "2", host], capture_output=True, text=True, timeout=float(timeout_s), ) except FileNotFoundError: return { "status": "error", "error": "traceroute_host: binario `traceroute` no encontrado en PATH", "host": host, "raw": "", } except subprocess.TimeoutExpired as exc: partial = exc.stdout or "" if isinstance(partial, bytes): partial = partial.decode(errors="replace") return { "status": "error", "error": f"traceroute_host: timeout duro del subprocess tras {timeout_s}s", "host": host, "raw": partial, } raw = proc.stdout or "" hops: list[dict] = [] for line in raw.splitlines(): m = _HOP_LINE_RE.match(line) if not m: continue # cabecera "traceroute to ..." u otras lineas no-hop hop_num = int(m.group(1)) hops.append(_parse_hop(hop_num, m.group(2))) return { "status": "ok", "host": host, "hops": hops, "raw": raw, } if __name__ == "__main__": try: result = traceroute_host("1.1.1.1", max_hops=15, timeout_s=40) print(result["status"]) if result["status"] == "ok": print(f"hops detectados: {len(result['hops'])}") for hop in result["hops"][:5]: ips = [h["ip"] for h in hop["hosts"] if h["ip"]] print(f" hop {hop['hop']}: {ips or '* * *'}") print("--- raw ---") print(result["raw"]) else: print("error:", result.get("error")) except Exception as exc: # smoke: tolera cualquier fallo de red sin romper print("smoke fallo (tolerado):", exc)