From 0245a008d14f46f33d397bfbb2034180e4e95168 Mon Sep 17 00:00:00 2001 From: feng <1304903146@qq.com> Date: Wed, 17 Jun 2026 16:20:07 +0800 Subject: [PATCH] perf: traceroute --- apps/settings/tools/traceroute.py | 77 ++++++++------- utils/debug_traceroute.py | 152 ++++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+), 33 deletions(-) create mode 100644 utils/debug_traceroute.py diff --git a/apps/settings/tools/traceroute.py b/apps/settings/tools/traceroute.py index 726f3a07d..9adc63b2d 100644 --- a/apps/settings/tools/traceroute.py +++ b/apps/settings/tools/traceroute.py @@ -1,12 +1,17 @@ # -*- coding: utf-8 -*- # import asyncio +import select import socket import struct import time +from common.utils import get_logger from settings.utils import generate_ips +logger = get_logger(__name__) +ICMP_PROTO = getattr(socket, 'IPPROTO_ICMP', 1) + def calculate_checksum(data): # 计算 ICMP 校验和 @@ -21,40 +26,45 @@ def calculate_checksum(data): return checksum +def recv_with_timeout(sock_fd, timeout): + ready, _, _ = select.select([sock_fd], [], [], timeout) + if not ready: + raise TimeoutError(f'recv timeout after {timeout}s') + return sock_fd.recvfrom(1024) + + async def once_traceroute(target, display, max_hops=30, timeout=3): - loop = asyncio.get_event_loop() - icmp = socket.getprotobyname('icmp') - sock_fd = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp) - for ttl in range(1, max_hops+1): - # 设置 TTL - sock_fd.setsockopt(socket.IPPROTO_IP, socket.IP_TTL, ttl) - # 设置超时时间 - sock_fd.settimeout(timeout) - # 发送 ICMP Echo 请求数据包 - packet = struct.pack('!BBHHH', 8, 0, 0, 12345, ttl) - checksum = calculate_checksum(packet) - packet = struct.pack('!BBHHH', 8, 0, checksum, 12345, ttl) - start_time = time.time() - sock_fd.sendto(packet, (target, 0)) - await asyncio.sleep(0.01) - try: - # 接收 ICMP Echo Reply 数据包 - recv_packet, addr = await loop.sock_recvfrom(sock_fd, 1024) - end_time = time.time() - # 解析目标地址 - dest_ip = addr[0] - # 获取跃点信息 - hop_info = f'{ttl} {dest_ip} ({dest_ip})' - # 获取延迟时间信息 - delay_info = f'{(end_time - start_time) * 1000:.3f} ms' - # 打印跃点信息和延迟时间 - await display(f'{hop_info:<4} {delay_info}') - if dest_ip == target: - return - except socket.timeout: - # 发生超时,跳出循环 - await display(f'{ttl} *') - sock_fd.close() + sock_fd = socket.socket(socket.AF_INET, socket.SOCK_RAW, ICMP_PROTO) + try: + for ttl in range(1, max_hops+1): + # 设置 TTL + sock_fd.setsockopt(socket.IPPROTO_IP, socket.IP_TTL, ttl) + # 发送 ICMP Echo 请求数据包 + packet = struct.pack('!BBHHH', 8, 0, 0, 12345, ttl) + checksum = calculate_checksum(packet) + packet = struct.pack('!BBHHH', 8, 0, checksum, 12345, ttl) + start_time = time.monotonic() + sock_fd.sendto(packet, (target, 0)) + await asyncio.sleep(0.01) + try: + # 使用 select 等待 raw socket 可读,避免与 loop.sock_recvfrom 混用 + recv_packet, addr = await asyncio.to_thread(recv_with_timeout, sock_fd, timeout) + end_time = time.monotonic() + # 解析目标地址 + dest_ip = addr[0] + # 获取跃点信息 + hop_info = f'{ttl} {dest_ip} ({dest_ip})' + # 获取延迟时间信息 + delay_info = f'{(end_time - start_time) * 1000:.3f} ms' + # 打印跃点信息和延迟时间 + await display(f'{hop_info:<4} {delay_info}') + if dest_ip == target: + return + except (TimeoutError, socket.timeout): + # 发生超时,跳出循环 + await display(f'{ttl} *') + finally: + sock_fd.close() async def verbose_traceroute(dest_ips, timeout=10, display=None): @@ -69,5 +79,6 @@ async def verbose_traceroute(dest_ips, timeout=10, display=None): try: await once_traceroute(dest_ip, display) except Exception as e: + logger.exception('Traceroute failed for %s', dest_ip) msg = f'Error: {e}' await display(msg) diff --git a/utils/debug_traceroute.py b/utils/debug_traceroute.py new file mode 100644 index 000000000..1e6a216c1 --- /dev/null +++ b/utils/debug_traceroute.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import argparse +import os +import platform +import select +import socket +import struct +import sys +import time +import traceback + + +def calculate_checksum(data): + checksum = 0 + if len(data) % 2 == 1: + data += b'\x00' + for i in range(0, len(data), 2): + checksum += (data[i] << 8) + data[i + 1] + checksum = (checksum >> 16) + (checksum & 0xFFFF) + checksum = ~checksum & 0xFFFF + return checksum + + +def build_icmp_packet(seq, ident=12345): + packet = struct.pack('!BBHHH', 8, 0, 0, ident, seq) + checksum = calculate_checksum(packet) + return struct.pack('!BBHHH', 8, 0, checksum, ident, seq) + + +def resolve_target(target): + print(f'[resolve] input={target}') + addrinfos = socket.getaddrinfo( + target, None, socket.AF_UNSPEC, socket.SOCK_DGRAM + ) + for index, info in enumerate(addrinfos, start=1): + family, socktype, proto, canonname, sockaddr = info + print( + '[resolve] #%s family=%s socktype=%s proto=%s canonname=%r sockaddr=%r' + % (index, family, socktype, proto, canonname, sockaddr) + ) + + family, _, _, _, sockaddr = addrinfos[0] + if family != socket.AF_INET: + raise RuntimeError(f'Only IPv4 is supported by this debug script, got family={family}') + resolved_ip = sockaddr[0] + print(f'[resolve] selected_ipv4={resolved_ip}') + return resolved_ip + + +def recv_with_timeout(sock_fd, timeout): + ready, _, _ = select.select([sock_fd], [], [], timeout) + if not ready: + raise TimeoutError(f'recv timeout after {timeout}s') + return sock_fd.recvfrom(1024) + + +def debug_traceroute(target, max_hops, timeout, pause): + print(f'[target] {target}') + resolved_ip = resolve_target(target) + + print("[step] socket.getprotobyname('icmp')") + icmp_proto = socket.getprotobyname('icmp') + print(f'[ok] icmp_proto={icmp_proto}') + + print('[step] socket(AF_INET, SOCK_RAW, icmp_proto)') + sock_fd = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp_proto) + print(f'[ok] raw_socket_fd={sock_fd.fileno()}') + + try: + for ttl in range(1, max_hops + 1): + print(f'\n[ttl={ttl}]') + + print(f'[step] setsockopt(IP_TTL={ttl})') + sock_fd.setsockopt(socket.IPPROTO_IP, socket.IP_TTL, ttl) + print('[ok] setsockopt') + + print(f'[step] settimeout({timeout})') + sock_fd.settimeout(timeout) + print('[ok] settimeout') + + packet = build_icmp_packet(ttl) + print(f'[step] sendto({resolved_ip}, 0), bytes={len(packet)}') + start_time = time.time() + sent_bytes = sock_fd.sendto(packet, (resolved_ip, 0)) + print(f'[ok] sent_bytes={sent_bytes}') + + if pause > 0: + time.sleep(pause) + + print(f'[step] recvfrom(timeout={timeout})') + recv_packet, addr = recv_with_timeout(sock_fd, timeout) + elapsed_ms = (time.time() - start_time) * 1000 + dest_ip = addr[0] + print( + f'[ok] addr={addr!r} bytes={len(recv_packet)} elapsed_ms={elapsed_ms:.3f}' + ) + print(f'[hop] {ttl} {dest_ip} ({dest_ip}) {elapsed_ms:.3f} ms') + + if dest_ip == resolved_ip: + print('[done] destination reached') + return 0 + + print('[done] max hops reached') + return 0 + finally: + sock_fd.close() + print('[cleanup] socket closed') + + +def main(): + parser = argparse.ArgumentParser( + description='Debug raw-socket traceroute step by step' + ) + parser.add_argument('target', help='IP or domain to trace') + parser.add_argument('--max-hops', type=int, default=5, help='Max hops to probe') + parser.add_argument('--timeout', type=float, default=3.0, help='Recv timeout in seconds') + parser.add_argument( + '--pause', + type=float, + default=0.01, + help='Pause between send and recv to mimic current implementation', + ) + parser.add_argument( + '--traceback', + action='store_true', + help='Print full traceback on failure', + ) + args = parser.parse_args() + + print(f'[env] python={sys.version.split()[0]}') + print(f'[env] platform={platform.platform()}') + print(f'[env] pid={os.getpid()} uid={os.getuid()} euid={os.geteuid()}') + print(f'[env] argv={sys.argv!r}') + + try: + return debug_traceroute( + target=args.target, + max_hops=args.max_hops, + timeout=args.timeout, + pause=args.pause, + ) + except Exception as exc: + print(f'[error] {type(exc).__name__}: {exc}') + if args.traceback: + traceback.print_exc() + return 1 + + +if __name__ == '__main__': + raise SystemExit(main())