diff --git a/backend/app/api/v1/endpoints/traffic.py b/backend/app/api/v1/endpoints/traffic.py index 2dc7045b..13e00e53 100644 --- a/backend/app/api/v1/endpoints/traffic.py +++ b/backend/app/api/v1/endpoints/traffic.py @@ -1,6 +1,6 @@ from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect, HTTPException from fastapi.responses import FileResponse -from typing import List, Dict +from typing import List, Dict, Optional from app.services.SnifferService import sniffer_service from app.schemas.traffic import StormConfig from pydantic import BaseModel @@ -9,11 +9,50 @@ import os import subprocess import time +import re +import requests router = APIRouter() class PingRequest(BaseModel): - host: str + target: str + protocol: str = "icmp" + port: Optional[int] = None + count: int = 4 + timeout: int = 5 + packet_size: int = 56 + use_https: bool = False + + # Validators + @staticmethod + def validate_target(v): + if not v or len(v) > 255: + raise ValueError("Target must be a valid hostname or IP address") + return v + + @staticmethod + def validate_port(v): + if v is not None and (v < 1 or v > 65535): + raise ValueError("Port must be between 1 and 65535") + return v + + @staticmethod + def validate_count(v): + if v < 1 or v > 100: + raise ValueError("Count must be between 1 and 100") + return v + + @staticmethod + def validate_timeout(v): + if v < 1 or v > 30: + raise ValueError("Timeout must be between 1 and 30 seconds") + return v + + @staticmethod + def validate_packet_size(v): + if v < 1 or v > 65500: + raise ValueError("Packet size must be between 1 and 65500 bytes") + return v @router.get("/interfaces") async def get_interfaces(): @@ -119,31 +158,384 @@ async def get_storm_metrics(): @router.post("/ping") async def ping_host(request: PingRequest): - """Ping a host to check reachability""" + """Advanced ping supporting multiple protocols: ICMP, TCP, UDP, HTTP, DNS""" try: - start_time = time.time() - result = subprocess.run( - ['ping', '-c', '1', '-W', '1', request.host], - capture_output=True, - text=True, - timeout=2 - ) - latency = (time.time() - start_time) * 1000 # Convert to ms + protocol = request.protocol.lower() + target = request.target + count = request.count + timeout = request.timeout + packet_size = request.packet_size - reachable = result.returncode == 0 + results = [] - return { - "host": request.host, - "reachable": reachable, - "latency": latency if reachable else None, - "last_check": time.strftime("%Y-%m-%d %H:%M:%S") - } - except subprocess.TimeoutExpired: - return { - "host": request.host, - "reachable": False, - "latency": None, - "last_check": time.strftime("%Y-%m-%d %H:%M:%S") - } + if protocol == "icmp": + # Standard ICMP ping + try: + cmd = ['ping', '-c', str(count), '-W', str(timeout), '-s', str(packet_size), target] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout + 2) + + # Parse output + output_lines = result.stdout.splitlines() + raw_output = result.stdout + + # Extract statistics + transmitted = received = 0 + packet_loss = 100.0 + min_ms = max_ms = avg_ms = None + + for line in output_lines: + # Parse transmitted/received + if "packets transmitted" in line: + match = re.search(r'(\d+) packets transmitted, (\d+) received', line) + if match: + transmitted = int(match.group(1)) + received = int(match.group(2)) + if transmitted > 0: + packet_loss = ((transmitted - received) / transmitted) * 100 + + # Parse RTT statistics + if "min/avg/max" in line or "rtt min/avg/max" in line: + match = re.search(r'= ([\d.]+)/([\d.]+)/([\d.]+)', line) + if match: + min_ms = float(match.group(1)) + avg_ms = float(match.group(2)) + max_ms = float(match.group(3)) + + # Parse individual ping results + if "icmp_seq=" in line: + seq_match = re.search(r'icmp_seq=(\d+)', line) + time_match = re.search(r'time=([\d.]+)', line) + if seq_match: + seq = int(seq_match.group(1)) + if time_match: + time_ms = float(time_match.group(1)) + results.append({"seq": seq, "status": "success", "time_ms": time_ms}) + else: + results.append({"seq": seq, "status": "timeout"}) + + # Fill in missing sequences as timeouts + for i in range(1, count + 1): + if not any(r["seq"] == i for r in results): + results.append({"seq": i, "status": "timeout"}) + + return { + "protocol": "icmp", + "target": target, + "count": count, + "transmitted": transmitted, + "received": received, + "successful": received, + "failed": transmitted - received, + "packet_loss": round(packet_loss, 1), + "min_ms": min_ms, + "max_ms": max_ms, + "avg_ms": avg_ms, + "results": sorted(results, key=lambda x: x["seq"]), + "raw_output": raw_output, + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S") + } + + except subprocess.TimeoutExpired: + return { + "protocol": "icmp", + "target": target, + "error": "Ping timed out", + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S") + } + except Exception as e: + return { + "protocol": "icmp", + "target": target, + "error": str(e), + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S") + } + + elif protocol == "tcp": + # TCP SYN ping using hping3 + if not request.port: + return { + "protocol": "tcp", + "target": target, + "error": "Port is required for TCP ping", + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S") + } + + try: + # hping3 -S -c count -p port -W timeout target + cmd = ['hping3', '-S', '-c', str(count), '-p', str(request.port), + '-W', str(timeout), target] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout + 2) + + output_lines = result.stdout + "\n" + result.stderr + + # Parse hping3 output + successful = 0 + failed = 0 + times = [] + + for i, line in enumerate(output_lines.splitlines()): + if "flags=" in line: + # Extract RTT + time_match = re.search(r'rtt=([\d.]+)', line) + if time_match: + time_ms = float(time_match.group(1)) + times.append(time_ms) + successful += 1 + results.append({"seq": i + 1, "status": "success", "time_ms": time_ms}) + else: + successful += 1 + results.append({"seq": i + 1, "status": "sent"}) + + failed = count - successful + + # Fill missing sequences + for i in range(1, count + 1): + if not any(r["seq"] == i for r in results): + results.append({"seq": i, "status": "timeout"}) + + response_data = { + "protocol": "tcp", + "target": target, + "port": request.port, + "count": count, + "successful": successful, + "failed": failed, + "packet_loss": round((failed / count) * 100, 1) if count > 0 else 0, + "results": sorted(results, key=lambda x: x["seq"]), + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S") + } + + if times: + response_data["min_ms"] = round(min(times), 2) + response_data["max_ms"] = round(max(times), 2) + response_data["avg_ms"] = round(sum(times) / len(times), 2) + + return response_data + + except FileNotFoundError: + return { + "protocol": "tcp", + "target": target, + "port": request.port, + "error": "hping3 not installed. Please install hping3 for TCP ping support.", + "note": "Run: apt-get install hping3", + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S") + } + except Exception as e: + return { + "protocol": "tcp", + "target": target, + "port": request.port, + "error": str(e), + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S") + } + + elif protocol == "udp": + # UDP ping using hping3 + if not request.port: + return { + "protocol": "udp", + "target": target, + "error": "Port is required for UDP ping", + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S") + } + + try: + cmd = ['hping3', '--udp', '-c', str(count), '-p', str(request.port), + '-W', str(timeout), target] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout + 2) + + output_lines = result.stdout + "\n" + result.stderr + + successful = 0 + times = [] + + for i, line in enumerate(output_lines.splitlines()): + if "ICMP Port Unreachable" in line or "flags=" in line: + time_match = re.search(r'rtt=([\d.]+)', line) + if time_match: + time_ms = float(time_match.group(1)) + times.append(time_ms) + successful += 1 + results.append({"seq": i + 1, "status": "success", "time_ms": time_ms}) + else: + successful += 1 + results.append({"seq": i + 1, "status": "sent"}) + + failed = count - successful + + for i in range(1, count + 1): + if not any(r["seq"] == i for r in results): + results.append({"seq": i, "status": "timeout"}) + + response_data = { + "protocol": "udp", + "target": target, + "port": request.port, + "count": count, + "successful": successful, + "failed": failed, + "packet_loss": round((failed / count) * 100, 1) if count > 0 else 0, + "results": sorted(results, key=lambda x: x["seq"]), + "note": "UDP ping shows response when port is unreachable (ICMP error) or open (data response)", + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S") + } + + if times: + response_data["min_ms"] = round(min(times), 2) + response_data["max_ms"] = round(max(times), 2) + response_data["avg_ms"] = round(sum(times) / len(times), 2) + + return response_data + + except FileNotFoundError: + return { + "protocol": "udp", + "target": target, + "port": request.port, + "error": "hping3 not installed. Please install hping3 for UDP ping support.", + "note": "Run: apt-get install hping3", + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S") + } + except Exception as e: + return { + "protocol": "udp", + "target": target, + "port": request.port, + "error": str(e), + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S") + } + + elif protocol == "http": + # HTTP/HTTPS ping + port = request.port or (443 if request.use_https else 80) + scheme = "https" if request.use_https else "http" + url = f"{scheme}://{target}:{port}" + + successful = 0 + failed = 0 + times = [] + + for i in range(1, count + 1): + try: + start = time.time() + # Note: SSL verification disabled for ping testing. In production, consider making this configurable. + response = requests.get(url, timeout=timeout, verify=False) + elapsed_ms = (time.time() - start) * 1000 + + times.append(elapsed_ms) + successful += 1 + results.append({ + "seq": i, + "status": "success", + "time_ms": round(elapsed_ms, 2), + "http_code": str(response.status_code) + }) + except requests.Timeout: + failed += 1 + results.append({"seq": i, "status": "timeout"}) + except Exception as e: + failed += 1 + results.append({"seq": i, "status": "failed", "error": str(e)}) + + response_data = { + "protocol": "http", + "target": target, + "port": port, + "count": count, + "successful": successful, + "failed": failed, + "packet_loss": round((failed / count) * 100, 1) if count > 0 else 0, + "results": results, + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S") + } + + if times: + response_data["min_ms"] = round(min(times), 2) + response_data["max_ms"] = round(max(times), 2) + response_data["avg_ms"] = round(sum(times) / len(times), 2) + + return response_data + + elif protocol == "dns": + # DNS query using dig + port = request.port or 53 + + try: + successful = 0 + failed = 0 + times = [] + + for i in range(1, count + 1): + try: + # Query for root domain to ensure compatibility with any DNS server + cmd = ['dig', f'@{target}', '-p', str(port), '.', 'NS', '+time=1'] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout) + + # Parse query time + time_match = re.search(r'Query time: (\d+) msec', result.stdout) + if time_match: + query_time = float(time_match.group(1)) + times.append(query_time) + successful += 1 + results.append({"seq": i, "status": "success", "time_ms": query_time}) + elif result.returncode == 0: + successful += 1 + results.append({"seq": i, "status": "success"}) + else: + failed += 1 + results.append({"seq": i, "status": "failed"}) + except subprocess.TimeoutExpired: + failed += 1 + results.append({"seq": i, "status": "timeout"}) + except Exception as e: + failed += 1 + results.append({"seq": i, "status": "failed"}) + + response_data = { + "protocol": "dns", + "target": target, + "port": port, + "count": count, + "successful": successful, + "failed": failed, + "packet_loss": round((failed / count) * 100, 1) if count > 0 else 0, + "results": results, + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S") + } + + if times: + response_data["min_ms"] = round(min(times), 2) + response_data["max_ms"] = round(max(times), 2) + response_data["avg_ms"] = round(sum(times) / len(times), 2) + + return response_data + + except FileNotFoundError: + return { + "protocol": "dns", + "target": target, + "port": port, + "error": "dig not installed. Please install dnsutils for DNS ping support.", + "note": "Run: apt-get install dnsutils", + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S") + } + except Exception as e: + return { + "protocol": "dns", + "target": target, + "port": port, + "error": str(e), + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S") + } + + else: + return { + "protocol": protocol, + "target": target, + "error": f"Unsupported protocol: {protocol}. Supported: icmp, tcp, udp, http, dns", + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S") + } + except Exception as e: raise HTTPException(status_code=500, detail=str(e)) diff --git a/backend/app/services/SnifferService.py b/backend/app/services/SnifferService.py index 654c012b..29b41ef8 100644 --- a/backend/app/services/SnifferService.py +++ b/backend/app/services/SnifferService.py @@ -19,6 +19,10 @@ class SnifferService: RESPONSE_HEX_MAX_LENGTH = 200 # characters STORM_THREAD_STOP_TIMEOUT = 2.0 # seconds + # Informational notes for packet crafting + NOTE_TCP_SYN_NO_RESPONSE = "Note: SYN packets may not receive responses if the target port is filtered by a firewall, the host is down, or the service is not listening. This is normal behavior for many hosts, especially public IPs like DNS servers (e.g., 8.8.8.8) which only respond to DNS queries on port 53." + NOTE_UDP_NO_RESPONSE = "Note: UDP is connectionless. No response may indicate the port is open but the service doesn't respond to empty packets, or the port is filtered by a firewall." + def __init__(self): self.is_sniffing = False self.capture_thread: Optional[threading.Thread] = None @@ -894,6 +898,7 @@ def craft_and_send_packet(self, packet_config: Dict[str, Any]) -> Dict[str, Any] # Send the packet(s) trace.append("Sending packet...") + trace.append(f"Packet summary: {packet.summary()}") start_time = time.time() # For continuous or multi-packet sending @@ -990,7 +995,15 @@ def craft_and_send_packet(self, packet_config: Dict[str, Any]) -> Dict[str, Any] } else: trace.append(f"No response received (timeout: {self.PACKET_SEND_TIMEOUT}s)") - return { + + # Add helpful note about why there might be no response + note = None + if protocol == "TCP" and flags and "SYN" in flags: + note = self.NOTE_TCP_SYN_NO_RESPONSE + elif protocol == "UDP": + note = self.NOTE_UDP_NO_RESPONSE + + result = { "success": True, "sent_packet": { "protocol": protocol, @@ -1003,6 +1016,12 @@ def craft_and_send_packet(self, packet_config: Dict[str, Any]) -> Dict[str, Any] "trace": trace } + if note: + result["note"] = note + trace.append(note) + + return result + except Exception as send_err: trace.append(f"Error during send: {str(send_err)}") return {