235 lines
7.8 KiB
Python
235 lines
7.8 KiB
Python
import socket
|
|
import threading
|
|
import time
|
|
import copy
|
|
from typing import Dict, Any, List
|
|
|
|
# Scapy is a third-party library. Install it with: pip install scapy
|
|
try:
|
|
from scapy.all import sniff, get_if_list
|
|
from scapy.config import conf
|
|
except ImportError:
|
|
print("Scapy is not installed. Please run 'pip install scapy'")
|
|
exit(1)
|
|
|
|
# Shared statistics dictionary, protected by a lock
|
|
# This is the Python equivalent of your 'volatile struct ss_t stat'
|
|
stats: Dict[str, Dict[str, Any]] = {
|
|
"pcap": {
|
|
"name": "PCAP",
|
|
"counter": 0,
|
|
"period_us": 0,
|
|
"min_us": 0,
|
|
"max_us": 0,
|
|
},
|
|
"socket": {
|
|
"name": "SOCKET",
|
|
"counter": 0,
|
|
"period_us": 0,
|
|
"min_us": 0,
|
|
"max_us": 0,
|
|
},
|
|
}
|
|
stats_lock = threading.Lock()
|
|
|
|
# --- Configuration ---
|
|
#TARGET_IP = "192.168.2.2"
|
|
TARGET_IP = "0.0.0.0"
|
|
TARGET_PORT = 60012
|
|
BPF_FILTER = f"udp and port {TARGET_PORT}"
|
|
STATS_UPDATE_INTERVAL_S = 0.5
|
|
WARMUP_PACKETS = 100 # Number of packets to ignore at the beginning
|
|
|
|
def list_interfaces() -> List[Any]:
|
|
"""Lists available network interfaces in a user-friendly way."""
|
|
print("Available network interfaces:")
|
|
# We use get_if_list() to get the names and then access full details
|
|
# from scapy's configuration object for better descriptions.
|
|
interfaces = []
|
|
if_list = get_if_list()
|
|
for i, if_name in enumerate(if_list):
|
|
iface = conf.ifaces.get(if_name)
|
|
if iface:
|
|
description = iface.description or "No description"
|
|
print(f"{i + 1}. {if_name} ({description})")
|
|
interfaces.append(iface)
|
|
return interfaces
|
|
|
|
def pcap_capture_thread(interface_name: str) -> None:
|
|
"""
|
|
Captures packets using Scapy and calculates inter-packet latency.
|
|
This function replaces the C++ CaptureThreadProc and packet_handler.
|
|
"""
|
|
print(f"PCAP thread started on interface '{interface_name}'...")
|
|
last_packet_time: float = 0.0
|
|
|
|
def process_packet(packet) -> None:
|
|
"""Callback function for each captured packet."""
|
|
nonlocal last_packet_time
|
|
|
|
current_time = packet.time
|
|
|
|
# We use a nonlocal variable to store the timestamp of the previous packet
|
|
if last_packet_time == 0.0:
|
|
last_packet_time = current_time
|
|
return
|
|
|
|
delta_s = current_time - last_packet_time
|
|
delta_us = int(delta_s * 1_000_000)
|
|
last_packet_time = current_time
|
|
|
|
with stats_lock:
|
|
stats["pcap"]["counter"] += 1
|
|
|
|
# Start calculating stats only after a warm-up period
|
|
if stats["pcap"]["counter"] > WARMUP_PACKETS:
|
|
stats["pcap"]["period_us"] = delta_us
|
|
|
|
current_min = stats["pcap"]["min_us"]
|
|
current_max = stats["pcap"]["max_us"]
|
|
|
|
if current_min == 0 or delta_us < current_min:
|
|
stats["pcap"]["min_us"] = delta_us
|
|
if delta_us > current_max:
|
|
stats["pcap"]["max_us"] = delta_us
|
|
else:
|
|
# Initialize min/max during warm-up
|
|
stats["pcap"]["min_us"] = delta_us
|
|
stats["pcap"]["max_us"] = delta_us
|
|
|
|
|
|
# sniff is the Scapy equivalent of pcap_loop.
|
|
# store=0 ensures packets are not stored in memory, for performance.
|
|
sniff(iface=interface_name, filter=BPF_FILTER, prn=process_packet, store=0)
|
|
|
|
|
|
def socket_receive_thread(host_ip: str, port: int) -> None:
|
|
"""
|
|
Receives UDP packets on a standard socket and measures latency.
|
|
This function replaces the C++ RcvThreadProc.
|
|
"""
|
|
print(f"Socket thread started on {host_ip}:{port}...")
|
|
|
|
# time.perf_counter() is the Python equivalent of QueryPerformanceCounter
|
|
last_receive_time: float = 0.0
|
|
|
|
try:
|
|
# AF_INET for IPv4, SOCK_DGRAM for UDP
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
sock.bind((host_ip, port))
|
|
except socket.error as e:
|
|
print(f"Error binding socket: {e}")
|
|
return
|
|
|
|
while True:
|
|
try:
|
|
# Buffer size, similar to the C++ code
|
|
sock.recvfrom(2048)
|
|
current_time = time.perf_counter()
|
|
|
|
if last_receive_time == 0.0:
|
|
last_receive_time = current_time
|
|
continue
|
|
|
|
delta_s = current_time - last_receive_time
|
|
delta_us = int(delta_s * 1_000_000)
|
|
last_receive_time = current_time
|
|
|
|
with stats_lock:
|
|
stats["socket"]["counter"] += 1
|
|
|
|
# Start calculating stats only after a warm-up period
|
|
if stats["socket"]["counter"] > WARMUP_PACKETS:
|
|
stats["socket"]["period_us"] = delta_us
|
|
|
|
current_min = stats["socket"]["min_us"]
|
|
current_max = stats["socket"]["max_us"]
|
|
|
|
if current_min == 0 or delta_us < current_min:
|
|
stats["socket"]["min_us"] = delta_us
|
|
if delta_us > current_max:
|
|
stats["socket"]["max_us"] = delta_us
|
|
else:
|
|
# Initialize min/max during warm-up
|
|
stats["socket"]["min_us"] = delta_us
|
|
stats["socket"]["max_us"] = delta_us
|
|
|
|
except socket.error as e:
|
|
# This can happen if the socket is closed or another error occurs
|
|
print(f"\nSocket error: {e}")
|
|
with stats_lock:
|
|
# Store the error code, similar to the WSAGetLastError logic
|
|
stats["socket"]["counter"] = f"Error: {e.errno}"
|
|
break
|
|
|
|
sock.close()
|
|
|
|
|
|
def main():
|
|
"""Main function to set up threads and display statistics."""
|
|
|
|
interfaces = list_interfaces()
|
|
if not interfaces:
|
|
print("\nNo interfaces found! Make sure Scapy is installed and you have privileges.")
|
|
return
|
|
|
|
try:
|
|
choice = int(input(f"Enter the interface number (1-{len(interfaces)}): "))
|
|
if not 1 <= choice <= len(interfaces):
|
|
print("Invalid number.")
|
|
return
|
|
selected_interface = conf.ifaces.get(get_if_list()[choice - 1])
|
|
except (ValueError, IndexError):
|
|
print("Invalid input.")
|
|
return
|
|
|
|
print(f"\nListening on '{selected_interface.description}'...")
|
|
|
|
# Create and start the PCAP capture thread
|
|
pcap_thread = threading.Thread(
|
|
target=pcap_capture_thread,
|
|
args=(selected_interface.name,),
|
|
daemon=True # Threads will exit when the main program exits
|
|
)
|
|
|
|
# Create and start the socket receive thread
|
|
socket_thread = threading.Thread(
|
|
target=socket_receive_thread,
|
|
args=(TARGET_IP, TARGET_PORT),
|
|
daemon=True
|
|
)
|
|
|
|
pcap_thread.start()
|
|
socket_thread.start()
|
|
|
|
print("\nStarting measurements... Press Ctrl+C to stop.\n")
|
|
time.sleep(2) # Give threads time to initialize
|
|
|
|
try:
|
|
while True:
|
|
# Use deepcopy to get a snapshot of the stats, preventing race
|
|
# conditions during printing. This is the Pythonic way to do
|
|
# what you did with memcpy.
|
|
with stats_lock:
|
|
stats_snapshot = copy.deepcopy(stats)
|
|
|
|
# Format the output string
|
|
line = "\r"
|
|
for key in ["pcap", "socket"]:
|
|
s = stats_snapshot[key]
|
|
line += (
|
|
f"{s['name']}: {str(s['counter']):>6} | "
|
|
f"period={s['period_us']:>7}µs | "
|
|
f"min={s['min_us']:>7}µs | "
|
|
f"max={s['max_us']:>7}µs "
|
|
)
|
|
|
|
print(line, end="", flush=True)
|
|
time.sleep(STATS_UPDATE_INTERVAL_S)
|
|
|
|
except KeyboardInterrupt:
|
|
print("\n\nStopping analyzer.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |