From 0d10ab811029aae78b30d072d0dd61a8945acfd1 Mon Sep 17 00:00:00 2001 From: "joaquin.navajas" Date: Mon, 11 May 2026 08:09:47 +0200 Subject: [PATCH] JNC Linux --- src/__pycache__/__init__.cpython-312.pyc | Bin 146 -> 160 bytes .../advanced_router.cpython-312.pyc | Bin 0 -> 40646 bytes .../__pycache__/diagnostics.cpython-312.pyc | Bin 2703 -> 2717 bytes .../network_manager.cpython-312.pyc | Bin 15855 -> 17073 bytes src/core/__pycache__/scanner.cpython-312.pyc | Bin 6708 -> 6722 bytes src/core/__pycache__/sniffer.cpython-312.pyc | Bin 5952 -> 5966 bytes .../vlan35_checker.cpython-312.pyc | Bin 0 -> 9322 bytes .../__pycache__/vlan_scanner.cpython-312.pyc | Bin 4691 -> 4705 bytes src/core/advanced_router.py | 996 ++++++++++++++++++ src/core/network_manager.py | 27 + src/core/vlan35_checker.py | 228 ++++ .../__pycache__/main_window.cpython-312.pyc | Bin 8732 -> 11777 bytes src/gui/__pycache__/styles.cpython-312.pyc | Bin 6537 -> 7530 bytes src/gui/main_window.py | 116 +- src/gui/styles.py | 149 +-- .../advanced_routing.cpython-312.pyc | Bin 0 -> 41521 bytes .../diagnostics_view.cpython-312.pyc | Bin 11799 -> 11813 bytes .../interface_list.cpython-312.pyc | Bin 6191 -> 8378 bytes .../__pycache__/ip_config.cpython-312.pyc | Bin 9768 -> 9782 bytes .../network_monitor.cpython-312.pyc | Bin 0 -> 8382 bytes .../__pycache__/port_scanner.cpython-312.pyc | Bin 18893 -> 18907 bytes .../__pycache__/scanner_view.cpython-312.pyc | Bin 10411 -> 10425 bytes .../__pycache__/sniffer_view.cpython-312.pyc | Bin 10434 -> 10448 bytes .../__pycache__/vlan35_check.cpython-312.pyc | Bin 0 -> 24926 bytes .../__pycache__/vlan_bridge.cpython-312.pyc | Bin 29391 -> 29405 bytes src/gui/widgets/advanced_routing.py | 765 ++++++++++++++ src/gui/widgets/interface_list.py | 42 +- src/gui/widgets/network_monitor.py | 177 ++++ src/gui/widgets/vlan35_check.py | 522 +++++++++ .../report_generator.cpython-312.pyc | Bin 5878 -> 5892 bytes vlan35_check_report.html | 233 ++++ 31 files changed, 3165 insertions(+), 90 deletions(-) create mode 100644 src/core/__pycache__/advanced_router.cpython-312.pyc create mode 100644 src/core/__pycache__/vlan35_checker.cpython-312.pyc create mode 100644 src/core/advanced_router.py create mode 100644 src/core/vlan35_checker.py create mode 100644 src/gui/widgets/__pycache__/advanced_routing.cpython-312.pyc create mode 100644 src/gui/widgets/__pycache__/network_monitor.cpython-312.pyc create mode 100644 src/gui/widgets/__pycache__/vlan35_check.cpython-312.pyc create mode 100644 src/gui/widgets/advanced_routing.py create mode 100644 src/gui/widgets/network_monitor.py create mode 100644 src/gui/widgets/vlan35_check.py create mode 100644 vlan35_check_report.html diff --git a/src/__pycache__/__init__.cpython-312.pyc b/src/__pycache__/__init__.cpython-312.pyc index 83d69a7222a6584aa4ecfafd5b96a13afed9db11..f896b42fe15ed5752c1ca123834ef342246fd394 100644 GIT binary patch delta 50 zcmbQlxPX!SG%qg~0}w>^MNZ^4X0)GZFDVvKl%HOdn44Iv@8##L>z7&*lAoVbJTbut E07|6}y8r+H delta 36 qcmZ3$IEj(_G%qg~0}wd;mz&6K%xE~#UXsJh&sjG(IWccyo)G|>+zC7Y diff --git a/src/core/__pycache__/advanced_router.cpython-312.pyc b/src/core/__pycache__/advanced_router.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e4ba1bd2e24d307b11a80b58d6bd0a7c8e7517ec GIT binary patch literal 40646 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 D8isKX literal 0 HcmV?d00001 diff --git a/src/core/__pycache__/diagnostics.cpython-312.pyc b/src/core/__pycache__/diagnostics.cpython-312.pyc index 5704771b8496db641eaa49e4b20c4a833a29fd75..c4d4850c36ede09448be9bb49027720c5513b2a9 100644 GIT binary patch delta 52 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 Ang9R* delta 1915 zcmZ{le{54#6vy9fU)#5J>sm9mbz8Jdw(%J2ST_g^9Bi<{26S_)AoIs4dvEYq`(8P( z-PTRQ#F0ppAMmVl*ZO5~CqTe8%{ff0+0y@CV8M5YKtV4gc`| z@#(qeo_p`P-+S)s2WQxyGrk+8rNsiaX#5dvX7jwSipA#}H}^V{?v!WP!vu%0TTq;H zf+AjVP@EtPOF1UHekC&|Gt1LEFlb~m>L@w>ZTCZkJ5SDk87lHAt~oaBR?6o8STD^9 z!@is-`}sCD$ejEFtE_vh<(QFDTaFuv6Io4f*=3AoQ>vaZ%$B~`&gP-fgw8FtofYS+ z9g89h@ayg?OyU!sORShzNQbNIa9em3VTlPv0a!(*LrX&?zbLI|UjC`n&PwyQr61j_ zhkspmhVA2V|Hh^&n%6_vkC^uM4z!1Ncg1@Sb{&EkdGKuQ;a#yI=;v4ceGidPRem=04_2oYbyzN>yK%U15gM=YMobUu94hP6QL>T5LE4TZe zM$>Yq3`HBu!BhNt<(@5++ER&@o@fsrO=Q%`L~cp4dx~z#ZphH&y@c$tHDOzPZ&lx_ zXUQBP93~_PNBHHc`a=quatY8$Kqc%WENfsN8~V{_IY(0pBx&>*bw%u&7E{d{hcLp2TYrs3t_JTUjAOq26lzts;OtC z{O_9m!P7Lwv67-m#S{u*q*B|!7WqrH594KCuALQo&*6wAj%&ICon(3@k_ml8Q!Mup z(3GR_$L?hm$rjLP!i;E-dQQ^bC-0 z@Ek(++L>A23}i9TE(VzAKd*jsxw7V2NALl zn#m3mT6tSj0HE4;?nG&hu#qZzZ+g4g+h=Fg9iR`4c)gnvMuqJlhbTbNd>@Ed_k7)7^&mosIF zh*p~sc%P=9LYPGe%%RXQlj(3L`yqcV{Fk$iipg?B+AUXlTuUbHSG81xPXVv-M0i~k zCi#uX3vII)2Fs_nTmwd0)gQ>sQ^>6dFH{A3#n8i_Z>z05N%QO+JZP8X_)F3NIBUNpZ494QPiZGQ5D&< zk|_MIm}21-ToNkr{7zJ@kfn%gQkE#v=}jBh_I!Sm=B!vw(UOiCW+stVMo5@QM}0GC zf`wyRA}M0ICP705><`2zl;nS^o6mKGymViM48P)w9iipg{-t9B+n=x48elxWV`vTO zh1F=k9M@>lFjb&yG#LWZqeX7=8#{tcS@f8G?9U3fq>>BLS?PlBtnWLi3QEx_i!p3q zuU5v(y6O+oR|fVFwh(3sX9zD6-X(lU_>Axc;akE@#DZY2S=gOEG(fP^it=~5c4~J9 z7;^;+K{O;66`oLpVmA^R3ASkBansN(DV+oQkBb036nj1Ro-9h<-e^hMhTrdOQtbo_ beRO`afuHQIF8@iWy(^|%EO=WW+Ee}kIyTZZ diff --git a/src/core/__pycache__/scanner.cpython-312.pyc b/src/core/__pycache__/scanner.cpython-312.pyc index ed9f817a4bbd82aecd06edcfa74f89769a35d1cd..6dd2ae84b970396b8ae8ea6da6455be6781d1d7b 100644 GIT binary patch delta 74 zcmdmDa>#`HG%qg~0}w>^MQ-GlV={2o&&bbB)z8XLEG*5;(|5^FF3nBNE6Fd`4=Bn{ cFG|c!EY|n(bJq1sEeXlb&ne#Q&Gb?n0A;Qj00000 delta 60 zcmX?Pvc-h^G%qg~0}uoR$#3MAV^T8J&&bbB)z8XLEG*5;(|5^FF3nBNE6Fd`_wsYr O4NgwX+Z@UCQXBvbDiY-Y diff --git a/src/core/__pycache__/sniffer.cpython-312.pyc b/src/core/__pycache__/sniffer.cpython-312.pyc index 71596267b0a77288043cd2a8f3f85525c5d29231..2760e6d99392888c2a5d351e7890d09d0d796543 100644 GIT binary patch delta 74 zcmX@0cTSJ{G%qg~0}w>^MQ-HgWioKq&&bbB)z8XLEG*5;(|5^FF3nBNE6Fd`4=Bn{ cFG|c!EY|n(bJq1sEeXlb&nez)#l$8G0Apep1poj5 delta 60 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 BACCY4 literal 0 HcmV?d00001 diff --git a/src/core/__pycache__/vlan_scanner.cpython-312.pyc b/src/core/__pycache__/vlan_scanner.cpython-312.pyc index 7c7311b1069a132732d406e4b566d4bd999060ec..ccd245a1a60dd6ce69384fdd683f76e1de5cbeae 100644 GIT binary patch delta 74 zcmcbt@=%5QG%qg~0}w>^MQ-HwV>0m2&&bbB)z8XLEG*5;(|5^FF3nBNE6Fd`4=Bn{ cFG|c!EY|n(bJq1sEeXlb&nezq&2)nw0D5~FY5)KL delta 60 zcmaE;a#@A@G%qg~0}u!V%WvfNV^Xrx&&bbB)z8XLEG*5;(|5^FF3nBNE6Fd`_wsYr O4NgwX+uX@?gC778+Y=A~ diff --git a/src/core/advanced_router.py b/src/core/advanced_router.py new file mode 100644 index 0000000..56053ba --- /dev/null +++ b/src/core/advanced_router.py @@ -0,0 +1,996 @@ +import json +import socket +import subprocess +from typing import Dict, List, Optional, Tuple + +from pyroute2 import IPRoute + +from src.core.network_manager import NetworkManager + + +class AdvancedRouter: + """ + Capa de alto nivel sobre pyroute2 / iptables / nftables + para ofrecer primitivas de routing avanzadas al GUI. + """ + + def __init__(self, network_manager: NetworkManager): + self.nm = network_manager + # Reutilizamos el mismo IPRoute que usa NetworkManager + self.ipr: IPRoute = network_manager.ipr + + # Estado en memoria para poder mostrar en la UI y guardar/cargar + self.vlans: Dict[str, Dict] = {} # clave: ifname VLAN (ej. eth0.10) + self.vlan_redirections: List[Dict] = [] + self.dhcp_servers: Dict[str, Dict] = {} # clave: interfaz sobre la que corre dnsmasq + self.gateways: Dict[str, Dict] = {} # clave: nombre interfaz o VLAN ifname + self.routes: List[Dict] = [] + self.routing_rules: List[Dict] = [] + self.nat_rules: List[Dict] = [] + self.firewall_rules: List[Dict] = [] + self.vlan_visibility: Dict[Tuple[int, int], bool] = {} # (src_vlan, dst_vlan) -> allowed + + self.firewall_backend = self.detect_firewall_backend() + + # Descubrir VLANs existentes en el sistema (por ejemplo eth0.10 creadas en sesiones anteriores) + self._discover_existing_vlans() + + def _discover_existing_vlans(self) -> None: + """ + Recorre los enlaces de pyroute2 y añade a self.vlans + todas las interfaces cuyo nombre tenga formato base.VLAN + (ej: enp1s0.10). Esto permite que VLANs creadas en una + sesión anterior se muestren y puedan borrarse. + """ + try: + links = self.ipr.get_links() + except Exception: + return + + for link in links: + ifname = link.get_attr("IFLA_IFNAME") + if not ifname: + continue + + if "." not in ifname: + continue + + base, dot, vlan_str = ifname.partition(".") + try: + vlan_id = int(vlan_str) + except ValueError: + continue + + if vlan_id < 1 or vlan_id > 4094: + continue + + # No machacamos una definición existente en memoria (por si viene de un load_config) + if ifname in self.vlans: + continue + + self.vlans[ifname] = { + "parent": base, + "vlan_id": vlan_id, + "mode": "trunk", + } + + # ------------------------------------------------------------------ + # VLANs y Trunks + # ------------------------------------------------------------------ + + def create_vlan_trunk(self, interface: str, vlan_ids: List[int]) -> Tuple[bool, str]: + """ + Crea múltiples interfaces VLAN tagged sobre una interfaz trunk. + No crea bridges todavía; eso se hará desde otras primitivas. + """ + try: + trunk_idx = self.ipr.link_lookup(ifname=interface)[0] + except IndexError: + return False, f"Interface {interface} not found" + + # Verificar que la interfaz esté UP antes de crear VLANs + try: + link_info = self.ipr.get_links(ifname=interface)[0] + operstate = link_info.get_attr("IFLA_OPERSTATE") + if operstate != "UP": + return False, f"La interfaz {interface} debe estar UP antes de crear VLANs. Estado actual: {operstate}" + except Exception: + pass # Si no podemos verificar el estado, continuamos de todas formas + + created = [] + for vid in vlan_ids: + if vid < 1 or vid > 4094: + return False, f"Invalid VLAN ID {vid}" + + vlan_ifname = f"{interface}.{vid}" + try: + # Si ya existe, simplemente la marcamos como conocida + if self.ipr.link_lookup(ifname=vlan_ifname): + self.vlans[vlan_ifname] = { + "parent": interface, + "vlan_id": vid, + "mode": "trunk", + } + created.append(vlan_ifname) + continue + + # Asegurar que vlan_id es un entero válido en el rango correcto + vlan_id_int = int(vid) + if vlan_id_int < 1 or vlan_id_int > 4094: + return False, f"VLAN ID {vid} fuera de rango válido (1-4094)" + + # Crear la VLAN usando la misma sintaxis que network_manager + self.ipr.link( + "add", + ifname=vlan_ifname, + kind="vlan", + link=trunk_idx, + vlan_id=vlan_id_int, + ) + vlan_idx = self.ipr.link_lookup(ifname=vlan_ifname)[0] + self.ipr.link("set", index=vlan_idx, state="up") + + self.vlans[vlan_ifname] = { + "parent": interface, + "vlan_id": vid, + "mode": "trunk", + } + created.append(vlan_ifname) + except IndexError: + # La interfaz no se creó correctamente + return False, f"No se pudo crear la interfaz VLAN {vlan_ifname}. Verifica que la interfaz base {interface} soporte VLANs." + except Exception as e: + error_msg = str(e) + # Mejorar el mensaje de error para casos comunes + if "34" in error_msg or "out of range" in error_msg.lower() or "ERANGE" in error_msg: + # Obtener lista de interfaces disponibles que podrían soportar VLANs + available_ifaces = [iface["name"] for iface in self.nm.list_interfaces() + if iface["name"] != interface and iface["type"] == "Wired"] + suggestions = "" + if available_ifaces: + suggestions = f"\n\nInterfaces alternativas disponibles que podrían soportar VLANs:\n" + ", ".join(available_ifaces[:5]) + + return False, ( + f"❌ La interfaz '{interface}' NO soporta VLAN tagging 802.1Q.\n\n" + f"Motivo: El driver de esta interfaz (probablemente USB Ethernet) no implementa " + f"soporte para VLANs a nivel de kernel.\n\n" + f"Soluciones:\n" + f"• Usa una interfaz Ethernet tradicional (no USB) como eth0, enp*, etc.\n" + f"• Si necesitas conectar redes, usa la pestaña 'VLAN / Puente' para crear " + f"bridges simples sin VLAN tagging.\n" + f"• Verifica el soporte de VLANs con: 'cat /proc/net/vlan/config'" + f"{suggestions}" + ) + return False, f"Error creating VLAN {vid} on {interface}: {error_msg}" + + return True, f"Created/validated VLANs on {interface}: {', '.join(created)}" + + def create_vlan_access(self, interface: str, vlan_id: int) -> Tuple[bool, str]: + """ + Configura una interfaz como acceso untagged a una VLAN. + A nivel Linux esto normalmente se modela como bridge + VLAN filter. + Aquí simplemente registramos el mapeo lógico; el bridge real se + gestiona en create_bridge_with_vlan_filters(). + """ + if vlan_id < 1 or vlan_id > 4094: + return False, "Invalid VLAN ID" + + self.vlans[interface] = { + "parent": interface, + "vlan_id": vlan_id, + "mode": "access", + } + return True, f"Interface {interface} marked as access for VLAN {vlan_id}" + + def delete_vlan(self, vlan_ifname: str) -> Tuple[bool, str]: + """ + Elimina una interfaz VLAN del sistema y limpia su estado interno. + También limpia redirecciones y configuraciones asociadas. + """ + try: + # Verificar si existe en el sistema + vlan_idx = self.ipr.link_lookup(ifname=vlan_ifname) + if vlan_idx: + # Primero bajar la interfaz + self.ipr.link("set", index=vlan_idx[0], state="down") + # Luego eliminarla + self.ipr.link("del", index=vlan_idx[0]) + + # Limpiar del estado interno + if vlan_ifname in self.vlans: + self.vlans.pop(vlan_ifname) + + # Limpiar redirecciones que usen esta VLAN + self.vlan_redirections = [ + r for r in self.vlan_redirections + if r.get("src_iface") != vlan_ifname + and r.get("dst_iface") != vlan_ifname + and not (r.get("src_vlan") and f"{r.get('src_iface', '')}.{r.get('src_vlan')}" == vlan_ifname) + and not (r.get("dst_vlan") and f"{r.get('dst_iface', '')}.{r.get('dst_vlan')}" == vlan_ifname) + ] + + # Limpiar gateways y DHCP asociados + if vlan_ifname in self.gateways: + self.gateways.pop(vlan_ifname) + if vlan_ifname in self.dhcp_servers: + # Detener DHCP si está corriendo + subprocess.run(["pkill", "-f", f"dnsmasq.*{vlan_ifname}"], check=False) + self.dhcp_servers.pop(vlan_ifname) + + return True, f"VLAN {vlan_ifname} eliminada correctamente" + except IndexError: + # La interfaz no existe en el sistema, pero la limpiamos del estado + if vlan_ifname in self.vlans: + self.vlans.pop(vlan_ifname) + return True, f"VLAN {vlan_ifname} eliminada del estado (no existía en el sistema)" + except Exception as e: + return False, f"Error eliminando VLAN {vlan_ifname}: {str(e)}" + + def redirect_vlan_tagged_to_untagged( + self, trunk_iface: str, vlan_id: int, access_iface: str + ) -> Tuple[bool, str]: + """ + Redirige una VLAN tagged de un trunk a una interfaz de acceso + creando (si es necesario) un bridge y asociando ambos puertos. + """ + return self.redirect_vlan_between_interfaces( + src_iface=trunk_iface, + src_vlan_id=vlan_id, + dst_iface=access_iface, + dst_vlan_id=None, + ) + + def redirect_vlan_untagged_to_tagged( + self, access_iface: str, vlan_id: int, trunk_iface: str + ) -> Tuple[bool, str]: + """ + Variante inversa: tráfico untagged de access_iface se mete en VLAN + concreta sobre el trunk. + """ + # Si la VLAN ya existe, usar método directo sin intentar crear nada + vlan_ifname = f"{trunk_iface}.{vlan_id}" + if self.ipr.link_lookup(ifname=vlan_ifname): + # La VLAN existe, crear bridge directo + return self._create_bridge_between_interfaces(access_iface, vlan_ifname) + else: + # Intentar método genérico + return self.redirect_vlan_between_interfaces( + src_iface=access_iface, + src_vlan_id=None, + dst_iface=trunk_iface, + dst_vlan_id=vlan_id, + ) + + def _create_bridge_between_interfaces( + self, iface1: str, iface2: str, bridge_name: Optional[str] = None + ) -> Tuple[bool, str]: + """ + Crea un bridge simple entre dos interfaces existentes. + No intenta crear VLANs ni nada adicional. + """ + try: + if not bridge_name: + bridge_name = f"br_{iface1.replace('.', '_')}_to_{iface2.replace('.', '_')}" + + # Verificar que ambas interfaces existen + idx1_list = self.ipr.link_lookup(ifname=iface1) + idx2_list = self.ipr.link_lookup(ifname=iface2) + + if not idx1_list: + return False, f"Interfaz '{iface1}' no existe" + if not idx2_list: + return False, f"Interfaz '{iface2}' no existe" + + idx1 = idx1_list[0] + idx2 = idx2_list[0] + + # Crear bridge si no existe + br_list = self.ipr.link_lookup(ifname=bridge_name) + if not br_list: + self.ipr.link("add", ifname=bridge_name, kind="bridge") + + br_idx = self.ipr.link_lookup(ifname=bridge_name)[0] + + # Añadir interfaces al bridge + self.ipr.link("set", index=idx1, master=br_idx) + self.ipr.link("set", index=idx2, master=br_idx) + + # Activar todo + self.ipr.link("set", index=br_idx, state="up") + self.ipr.link("set", index=idx1, state="up") + self.ipr.link("set", index=idx2, state="up") + + return True, f"Bridge {bridge_name} creado entre {iface1} y {iface2}" + except Exception as e: + return False, f"Error creando bridge: {str(e)}" + + def redirect_vlan_between_interfaces( + self, + src_iface: str, + src_vlan_id: Optional[int], + dst_iface: str, + dst_vlan_id: Optional[int], + ) -> Tuple[bool, str]: + """ + Redirección genérica entre interfaces (+/- VLAN). Implementación + basada en bridges: se crea un bridge específico y se conectan + las interfaces/ subinterfaces correspondientes. + + Si la VLAN destino ya existe (ej: enp122s0.10), la usa directamente + sin intentar crearla. Esto permite conectar interfaces que no soportan + VLANs (como USB Ethernet) a VLANs existentes. + """ + if src_iface == dst_iface and src_vlan_id == dst_vlan_id: + return False, "Source and destination cannot be identical" + + try: + # Determinar nombres de interfaces finales + src_ifname = src_iface + dst_ifname = dst_iface + + # Origen: NUNCA intentar crear VLAN si src_vlan_id es None (untagged) + # Si src_vlan_id está especificado, verificar si existe primero + if src_vlan_id is not None: + vlan_ifname = f"{src_iface}.{src_vlan_id}" + # Verificar si ya existe + if self.ipr.link_lookup(ifname=vlan_ifname): + src_ifname = vlan_ifname + else: + # Intentar crear solo si la interfaz base soporta VLANs + # Si falla, usamos la interfaz base directamente + ok, msg = self.create_vlan_trunk(src_iface, [src_vlan_id]) + if ok: + src_ifname = vlan_ifname + # Si falla, dejamos src_ifname = src_iface (untagged) + # No retornamos error aquí, simplemente usamos la base + + # Destino: verificar si la VLAN ya existe ANTES de intentar crearla + if dst_vlan_id is not None: + vlan_ifname = f"{dst_iface}.{dst_vlan_id}" + # Verificar si ya existe + existing_vlan = self.ipr.link_lookup(ifname=vlan_ifname) + if existing_vlan: + # La VLAN ya existe, usarla directamente + dst_ifname = vlan_ifname + else: + # La VLAN no existe, intentar crearla + ok, msg = self.create_vlan_trunk(dst_iface, [dst_vlan_id]) + if ok: + dst_ifname = vlan_ifname + else: + return False, f"No se pudo crear/verificar VLAN destino {vlan_ifname}: {msg}" + + # Si el origen es untagged y el destino es una VLAN existente, + # usar método directo de bridge simple + if src_vlan_id is None and dst_vlan_id is not None: + vlan_ifname = f"{dst_iface}.{dst_vlan_id}" + if self.ipr.link_lookup(ifname=vlan_ifname): + # La VLAN destino existe, usar método directo + ok, msg = self._create_bridge_between_interfaces(src_iface, vlan_ifname) + if ok: + rule = { + "src_iface": src_iface, + "src_vlan": None, + "dst_iface": dst_iface, + "dst_vlan": dst_vlan_id, + "bridge": msg.split(" ")[1] if "Bridge" in msg else f"br_{src_iface}_to_{vlan_ifname.replace('.', '_')}", + } + self.vlan_redirections.append(rule) + return ok, msg + + # Verificar que ambas interfaces existen antes de crear el bridge + try: + src_idx_list = self.ipr.link_lookup(ifname=src_ifname) + dst_idx_list = self.ipr.link_lookup(ifname=dst_ifname) + + if not src_idx_list: + return False, f"Interfaz origen '{src_ifname}' no existe" + if not dst_idx_list: + return False, f"Interfaz destino '{dst_ifname}' no existe" + + src_idx = src_idx_list[0] + dst_idx = dst_idx_list[0] + except (IndexError, Exception) as e: + return False, f"Error verificando interfaces: origen={src_ifname}, destino={dst_ifname}, error={str(e)}" + + # Crear bridge para esta redirección + bridge_name = f"br_{src_ifname.replace('.', '_')}_to_{dst_ifname.replace('.', '_')}" + + # Verificar si el bridge ya existe + existing_bridge = self.ipr.link_lookup(ifname=bridge_name) + if not existing_bridge: + self.ipr.link("add", ifname=bridge_name, kind="bridge") + + br_idx = self.ipr.link_lookup(ifname=bridge_name)[0] + + # Añadir interfaces al bridge + self.ipr.link("set", index=src_idx, master=br_idx) + self.ipr.link("set", index=dst_idx, master=br_idx) + + # Activar todo + self.ipr.link("set", index=br_idx, state="up") + self.ipr.link("set", index=src_idx, state="up") + self.ipr.link("set", index=dst_idx, state="up") + + rule = { + "src_iface": src_iface, + "src_vlan": src_vlan_id, + "dst_iface": dst_iface, + "dst_vlan": dst_vlan_id, + "bridge": bridge_name, + } + self.vlan_redirections.append(rule) + return True, f"Bridge {bridge_name} creado: {src_ifname} → {dst_ifname}" + except Exception as e: + error_str = str(e) + # Mejorar mensaje de error + if "34" in error_str or "ERANGE" in error_str or "out of range" in error_str.lower(): + return False, ( + f"Error: No se pudo crear el bridge. Posibles causas:\n" + f"• La interfaz origen '{src_iface}' no puede ser parte de un bridge con VLANs\n" + f"• La interfaz destino '{dst_iface}' no soporta la operación\n" + f"• Intenta usar la pestaña 'VLAN / Puente' para crear un bridge simple" + ) + return False, f"Error en redirección: {error_str}" + + # ------------------------------------------------------------------ + # Bridges y VLAN filters (esqueleto, se extenderá en tareas siguientes) + # ------------------------------------------------------------------ + + def create_bridge_with_vlan_filters( + self, bridge_name: str, interfaces: List[str], vlan_filters: List[Dict] + ) -> Tuple[bool, str]: + """ + Crea un bridge y aplica filtros VLAN en sus puertos. + vlan_filters: lista de dicts con claves: + { 'iface': str, 'vid': int, 'pvid': bool, 'untagged': bool } + """ + try: + if not self.ipr.link_lookup(ifname=bridge_name): + self.ipr.link("add", ifname=bridge_name, kind="bridge") + + br_idx = self.ipr.link_lookup(ifname=bridge_name)[0] + + for iface in interfaces: + idx = self.ipr.link_lookup(ifname=iface)[0] + self.ipr.link("set", index=idx, master=br_idx) + self.ipr.link("set", index=idx, state="up") + + self.ipr.link("set", index=br_idx, state="up") + + # Aplicar filtros VLAN sobre cada puerto + for vf in vlan_filters: + try: + port_idx = self.ipr.link_lookup(ifname=vf["iface"])[0] + vid = int(vf["vid"]) + flags = [] + if vf.get("pvid"): + flags.append("pvid") + if vf.get("untagged"): + flags.append("untagged") + + vlan_info = {"vid": vid} + if flags: + vlan_info["flags"] = flags + + # pyroute2 expone vlan_filter desde ipr + self.ipr.vlan_filter( + "add", + index=port_idx, + vlan_info=vlan_info, + ) + except Exception: + # No abortamos por un fallo puntual de filtro; se puede depurar luego + continue + + return True, f"Bridge {bridge_name} created with VLAN filters" + except Exception as e: + return False, str(e) + + def add_vlan_filter_to_bridge_port( + self, bridge_name: str, port_iface: str, vlan_id: int, pvid: bool = False, untagged: bool = False + ) -> Tuple[bool, str]: + try: + if not self.ipr.link_lookup(ifname=bridge_name): + return False, f"Bridge {bridge_name} not found" + + port_idx = self.ipr.link_lookup(ifname=port_iface)[0] + flags = [] + if pvid: + flags.append("pvid") + if untagged: + flags.append("untagged") + vlan_info = {"vid": int(vlan_id)} + if flags: + vlan_info["flags"] = flags + + self.ipr.vlan_filter("add", index=port_idx, vlan_info=vlan_info) + return True, "VLAN filter added" + except Exception as e: + return False, str(e) + + def remove_vlan_filter_from_bridge_port( + self, bridge_name: str, port_iface: str, vlan_id: int + ) -> Tuple[bool, str]: + try: + if not self.ipr.link_lookup(ifname=bridge_name): + return False, f"Bridge {bridge_name} not found" + + port_idx = self.ipr.link_lookup(ifname=port_iface)[0] + self.ipr.vlan_filter("del", index=port_idx, vlan_info={"vid": int(vlan_id)}) + return True, "VLAN filter removed" + except Exception as e: + return False, str(e) + + # ------------------------------------------------------------------ + # DHCP y Gateways + # ------------------------------------------------------------------ + + def start_dhcp_server_for_vlan( + self, + vlan_interface: str, + ip: str, + range_start: str, + range_end: str, + gateway: Optional[str] = None, + dns: Optional[str] = None, + ) -> Tuple[bool, str]: + """ + Envuelve a NetworkManager.start_dhcp_server y además registra gateway/dns. + """ + ok, msg = self.nm.start_dhcp_server(vlan_interface, ip, range_start, range_end) + if not ok: + return ok, msg + + self.dhcp_servers[vlan_interface] = { + "ip": ip, + "range_start": range_start, + "range_end": range_end, + } + + if gateway: + self.set_gateway_for_network(vlan_interface, ip, gateway) + if dns: + self.gateways.setdefault(vlan_interface, {})["dns"] = dns + + return True, msg + + def stop_dhcp_server_for_vlan(self, vlan_interface: str) -> Tuple[bool, str]: + try: + subprocess.run(["pkill", "-f", f"dnsmasq.*{vlan_interface}"], check=False) + self.dhcp_servers.pop(vlan_interface, None) + return True, f"DHCP server on {vlan_interface} stopped" + except Exception as e: + return False, str(e) + + def list_active_dhcp_servers(self) -> Dict[str, Dict]: + return dict(self.dhcp_servers) + + def set_gateway_for_network(self, interface: str, ip: str, gateway: str) -> Tuple[bool, str]: + self.gateways[interface] = {"ip": ip, "gateway": gateway} + # A nivel de sistema, añadimos/actualizamos ruta por defecto para esa red + try: + self.add_route(dst="0.0.0.0/0", gateway=gateway, interface=interface) + return True, f"Gateway {gateway} set for {interface}" + except Exception as e: + return False, str(e) + + def remove_gateway_for_network(self, interface: str, ip: str) -> Tuple[bool, str]: + if interface in self.gateways: + gw = self.gateways[interface].get("gateway") + self.gateways.pop(interface, None) + if gw: + try: + self.delete_route(dst="0.0.0.0/0", gateway=gw, interface=interface) + except Exception: + pass + return True, f"Gateway removed for {interface}" + + def list_gateways(self) -> Dict[str, Dict]: + return dict(self.gateways) + + # ------------------------------------------------------------------ + # Routing y tablas + # ------------------------------------------------------------------ + + def add_route( + self, + dst: str, + gateway: str, + interface: str, + table: int = 254, + metric: Optional[int] = None, + ) -> Tuple[bool, str]: + try: + idx = self.ipr.link_lookup(ifname=interface)[0] + dst_ip, prefix = dst.split("/") + attrs = [("OIF", idx), ("GATEWAY", gateway)] + self.ipr.route( + "add", + dst=dst_ip, + mask=int(prefix), + table=table, + metrics={"metric": metric} if metric is not None else None, + attrs=attrs, + ) + self.routes.append( + { + "dst": dst, + "gateway": gateway, + "interface": interface, + "table": table, + "metric": metric, + } + ) + return True, "Route added" + except Exception as e: + return False, str(e) + + def delete_route( + self, + dst: str, + gateway: str, + interface: str, + table: int = 254, + ) -> Tuple[bool, str]: + try: + idx = self.ipr.link_lookup(ifname=interface)[0] + dst_ip, prefix = dst.split("/") + self.ipr.route( + "del", + dst=dst_ip, + mask=int(prefix), + table=table, + attrs=[("OIF", idx), ("GATEWAY", gateway)], + ) + self.routes = [ + r + for r in self.routes + if not ( + r["dst"] == dst + and r["gateway"] == gateway + and r["interface"] == interface + and r["table"] == table + ) + ] + return True, "Route deleted" + except Exception as e: + return False, str(e) + + def add_routing_rule( + self, + table: int, + priority: int, + src: Optional[str] = None, + dst: Optional[str] = None, + iifname: Optional[str] = None, + oifname: Optional[str] = None, + ) -> Tuple[bool, str]: + try: + kwargs = {"table": table, "priority": priority} + if src: + ip, plen = src.split("/") + kwargs["src"] = ip + kwargs["src_len"] = int(plen) + if dst: + ip, plen = dst.split("/") + kwargs["dst"] = ip + kwargs["dst_len"] = int(plen) + if iifname: + kwargs["iifname"] = iifname + if oifname: + kwargs["oifname"] = oifname + + self.ipr.rule("add", **kwargs) + self.routing_rules.append(kwargs) + return True, "Routing rule added" + except Exception as e: + return False, str(e) + + def list_routes(self, table: Optional[int] = None) -> List[Dict]: + if table is None: + return list(self.routes) + return [r for r in self.routes if r["table"] == table] + + def enable_ip_forwarding(self) -> Tuple[bool, str]: + try: + with open("/proc/sys/net/ipv4/ip_forward", "w") as f: + f.write("1") + return True, "IP forwarding enabled" + except Exception as e: + return False, str(e) + + # ------------------------------------------------------------------ + # NAT y Port Forwarding + # ------------------------------------------------------------------ + + def detect_firewall_backend(self) -> str: + """ + Devuelve 'nftables', 'iptables' o 'none'. + """ + from shutil import which + + if which("nft"): + return "nftables" + if which("iptables"): + return "iptables" + return "none" + + def _run_iptables(self, args: List[str]) -> None: + subprocess.run(["iptables"] + args, check=True) + + def _run_nft(self, args: List[str]) -> None: + subprocess.run(["nft"] + args, check=True) + + def enable_nat_for_interface(self, interface: str, external_iface: str) -> Tuple[bool, str]: + """ + Habilita SNAT/MASQUERADE para que las redes internas salgan por external_iface. + Implementación mínima usando iptables o nftables. + """ + try: + if self.firewall_backend == "iptables": + # Habilitar forwarding + self._run_iptables(["-A", "FORWARD", "-i", interface, "-o", external_iface, "-j", "ACCEPT"]) + self._run_iptables(["-A", "FORWARD", "-i", external_iface, "-o", interface, "-m", "state", "--state", "ESTABLISHED,RELATED", "-j", "ACCEPT"]) + # NAT + self._run_iptables(["-t", "nat", "-A", "POSTROUTING", "-o", external_iface, "-j", "MASQUERADE"]) + elif self.firewall_backend == "nftables": + # Asumimos tabla inet nat preexistente o intentamos crear algo simple + self._run_nft(["add", "table", "inet", "jnc_nat"]) + self._run_nft(["add", "chain", "inet", "jnc_nat", "postrouting", "{", "type", "nat", "hook", "postrouting", "priority", "100", ";", "}"]) + self._run_nft( + [ + "add", + "rule", + "inet", + "jnc_nat", + "postrouting", + "oifname", + external_iface, + "masquerade", + ] + ) + else: + return False, "No firewall backend (iptables/nftables) available" + + self.nat_rules.append({"interface": interface, "external": external_iface}) + return True, "NAT enabled" + except Exception as e: + return False, str(e) + + def disable_nat_for_interface(self, interface: str) -> Tuple[bool, str]: + # Implementación sencilla: no sabemos exactamente qué reglas borrar, + # así que por ahora dejamos registro lógico y se puede extender. + self.nat_rules = [r for r in self.nat_rules if r["interface"] != interface] + return True, "NAT rules removed from internal state (system rules may persist)" + + def add_port_forwarding( + self, + external_ip: str, + external_port: int, + internal_ip: str, + internal_port: int, + protocol: str = "tcp", + ) -> Tuple[bool, str]: + try: + if self.firewall_backend == "iptables": + self._run_iptables( + [ + "-t", + "nat", + "-A", + "PREROUTING", + "-p", + protocol, + "-d", + external_ip, + "--dport", + str(external_port), + "-j", + "DNAT", + "--to-destination", + f"{internal_ip}:{internal_port}", + ] + ) + elif self.firewall_backend == "nftables": + self._run_nft(["add", "table", "inet", "jnc_nat"]) + self._run_nft(["add", "chain", "inet", "jnc_nat", "prerouting", "{", "type", "nat", "hook", "prerouting", "priority", "0", ";", "}"]) + self._run_nft( + [ + "add", + "rule", + "inet", + "jnc_nat", + "prerouting", + "ip", + "daddr", + external_ip, + "tcp", + "dport", + str(external_port), + "dnat", + "to", + f"{internal_ip}:{internal_port}", + ] + ) + else: + return False, "No firewall backend (iptables/nftables) available" + + rule = { + "external_ip": external_ip, + "external_port": external_port, + "internal_ip": internal_ip, + "internal_port": internal_port, + "protocol": protocol, + } + self.nat_rules.append(rule) + return True, "Port forwarding rule added" + except Exception as e: + return False, str(e) + + # ------------------------------------------------------------------ + # Firewall / Visibilidad entre VLANs + # ------------------------------------------------------------------ + + def add_firewall_rule( + self, + chain: str, + action: str, + src_network: Optional[str] = None, + dst_network: Optional[str] = None, + protocol: Optional[str] = None, + src_port: Optional[int] = None, + dst_port: Optional[int] = None, + ) -> Tuple[bool, str]: + """ + Regla genérica simple sobre FORWARD/INPUT/OUTPUT. + """ + try: + if self.firewall_backend == "iptables": + args = ["-A", chain] + if src_network: + args += ["-s", src_network] + if dst_network: + args += ["-d", dst_network] + if protocol: + args += ["-p", protocol] + if src_port: + args += ["--sport", str(src_port)] + if dst_port: + args += ["--dport", str(dst_port)] + args += ["-j", action] + self._run_iptables(args) + elif self.firewall_backend == "nftables": + # Implementación básica centrada en cadena forward de tabla inet filter + self._run_nft(["add", "table", "inet", "jnc_filter"]) + self._run_nft(["add", "chain", "inet", "jnc_filter", "forward", "{", "type", "filter", "hook", "forward", "priority", "0", ";", "}"]) + rule_parts = ["add", "rule", "inet", "jnc_filter", "forward"] + if src_network: + rule_parts += ["ip", "saddr", src_network] + if dst_network: + rule_parts += ["ip", "daddr", dst_network] + if protocol: + rule_parts.append(protocol) + if dst_port: + rule_parts += ["dport", str(dst_port)] + rule_parts.append(action.lower()) + self._run_nft(rule_parts) + else: + return False, "No firewall backend (iptables/nftables) available" + + rule = { + "chain": chain, + "action": action, + "src": src_network, + "dst": dst_network, + "protocol": protocol, + "src_port": src_port, + "dst_port": dst_port, + } + self.firewall_rules.append(rule) + return True, "Firewall rule added" + except Exception as e: + return False, str(e) + + def block_vlan_to_vlan(self, src_vlan_id: int, dst_vlan_id: int) -> Tuple[bool, str]: + src_net = f"10.{src_vlan_id}.0.0/16" + dst_net = f"10.{dst_vlan_id}.0.0/16" + ok, msg = self.add_firewall_rule( + chain="FORWARD", + action="DROP", + src_network=src_net, + dst_network=dst_net, + ) + if ok: + self.vlan_visibility[(src_vlan_id, dst_vlan_id)] = False + return ok, msg + + def allow_vlan_to_vlan(self, src_vlan_id: int, dst_vlan_id: int) -> Tuple[bool, str]: + src_net = f"10.{src_vlan_id}.0.0/16" + dst_net = f"10.{dst_vlan_id}.0.0/16" + ok, msg = self.add_firewall_rule( + chain="FORWARD", + action="ACCEPT", + src_network=src_net, + dst_network=dst_net, + ) + if ok: + self.vlan_visibility[(src_vlan_id, dst_vlan_id)] = True + return ok, msg + + def set_vlan_isolation_mode(self, vlan_id: int, isolated: bool = True) -> Tuple[bool, str]: + """ + Implementación mínima: registra el estado lógico. Las reglas reales + se pueden derivar en la UI según las necesidades del despliegue. + """ + if isolated: + # Bloqueamos VLAN contra todas las demás de forma lógica + for other in range(1, 4095): + if other == vlan_id: + continue + self.vlan_visibility[(vlan_id, other)] = False + self.vlan_visibility[(other, vlan_id)] = False + return True, "VLAN marked as isolated (logical state)" + else: + # Permitimos de nuevo (sólo borra del estado interno) + self.vlan_visibility = { + k: v + for k, v in self.vlan_visibility.items() + if vlan_id not in k + } + return True, "VLAN isolation disabled in logical state" + + def list_firewall_rules(self) -> List[Dict]: + return list(self.firewall_rules) + + # ------------------------------------------------------------------ + # Gestión de estado + # ------------------------------------------------------------------ + + def get_routing_status(self) -> Dict: + return { + "vlans": self.vlans, + "vlan_redirections": self.vlan_redirections, + "dhcp_servers": self.dhcp_servers, + "gateways": self.gateways, + "routes": self.routes, + "routing_rules": self.routing_rules, + "nat_rules": self.nat_rules, + "firewall_rules": self.firewall_rules, + "vlan_visibility": {f"{a}-{b}": allowed for (a, b), allowed in self.vlan_visibility.items()}, + } + + def save_routing_config(self, filename: str) -> Tuple[bool, str]: + try: + data = self.get_routing_status() + with open(filename, "w") as f: + json.dump(data, f, indent=2) + return True, filename + except Exception as e: + return False, str(e) + + def load_routing_config(self, filename: str) -> Tuple[bool, str]: + try: + with open(filename, "r") as f: + data = json.load(f) + # Sólo restauramos estado en memoria; aplicar en sistema sería más complejo + self.vlans = data.get("vlans", {}) + self.vlan_redirections = data.get("vlan_redirections", []) + self.dhcp_servers = data.get("dhcp_servers", {}) + self.gateways = data.get("gateways", {}) + self.routes = data.get("routes", []) + self.routing_rules = data.get("routing_rules", []) + self.nat_rules = data.get("nat_rules", []) + self.firewall_rules = data.get("firewall_rules", []) + vis = data.get("vlan_visibility", {}) + self.vlan_visibility = {} + for key, allowed in vis.items(): + try: + a_str, b_str = key.split("-") + self.vlan_visibility[(int(a_str), int(b_str))] = bool(allowed) + except Exception: + continue + return True, "Routing configuration loaded into memory (no live apply)" + except Exception as e: + return False, str(e) + diff --git a/src/core/network_manager.py b/src/core/network_manager.py index bf4f726..55eebbe 100644 --- a/src/core/network_manager.py +++ b/src/core/network_manager.py @@ -4,6 +4,8 @@ import struct import psutil import subprocess import json +import time +from typing import Tuple from pyroute2 import IPRoute class NetworkManager: @@ -370,5 +372,30 @@ rsn_pairwise=CCMP except Exception as e: return False, str(e) + def reset_interface(self, iface_name: str) -> Tuple[bool, str]: + """ + Reinicia una interfaz de red haciendo down y luego up. + Útil cuando una interfaz se queda "tonta" y necesita reiniciarse. + """ + try: + iface_idx = self.ipr.link_lookup(ifname=iface_name) + if not iface_idx: + return False, f"Interfaz {iface_name} no encontrada" + + idx = iface_idx[0] + + # Bajar la interfaz + self.ipr.link("set", index=idx, state="down") + + # Pequeña pausa para asegurar que el down se procesa + time.sleep(0.5) + + # Subir la interfaz + self.ipr.link("set", index=idx, state="up") + + return True, f"Interfaz {iface_name} reiniciada correctamente (down → up)" + except Exception as e: + return False, f"Error al reiniciar interfaz {iface_name}: {str(e)}" + def close(self): self.ipr.close() diff --git a/src/core/vlan35_checker.py b/src/core/vlan35_checker.py new file mode 100644 index 0000000..cd0e8fa --- /dev/null +++ b/src/core/vlan35_checker.py @@ -0,0 +1,228 @@ +import socket +import subprocess +import time +from datetime import datetime +from typing import Dict, List, Tuple, Optional +from PyQt6.QtCore import QThread, pyqtSignal + + +class ConnectivityTestThread(QThread): + """Thread para ejecutar tests de conectividad en paralelo.""" + test_result = pyqtSignal(dict) # Emite resultado de cada test + all_finished = pyqtSignal() # Emite cuando todos terminan + + def __init__(self, tests: List[Dict]): + super().__init__() + self.tests = tests + self.results = [] + + def run(self): + for test in self.tests: + result = self._run_test(test) + self.results.append(result) + self.test_result.emit(result) + self.all_finished.emit() + + def _run_test(self, test: Dict) -> Dict: + """Ejecuta un test individual y retorna resultado.""" + host = test["host"] + port = test["port"] + protocol = test.get("protocol", "TCP") + test_type = test.get("type", "connectivity") + + result = { + "host": host, + "port": port, + "protocol": protocol, + "type": test_type, + "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "success": False, + "status": "Unknown", + "latency_ms": None, + "error": None, + "details": "", + } + + try: + if protocol == "TCP": + result.update(self._test_tcp(host, port)) + elif protocol == "UDP": + if test_type == "ntp": + result.update(self._test_ntp(host)) + else: + result.update(self._test_udp(host, port)) + else: + result["error"] = f"Protocolo desconocido: {protocol}" + except Exception as e: + result["error"] = str(e) + result["status"] = "Error" + + return result + + def _test_tcp(self, host: str, port: int) -> Dict: + """Test de conectividad TCP.""" + start_time = time.time() + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(5) + result = sock.connect_ex((host, port)) + latency = (time.time() - start_time) * 1000 + sock.close() + + if result == 0: + return { + "success": True, + "status": "Open", + "latency_ms": round(latency, 2), + "details": f"Conexión TCP exitosa en {latency:.2f}ms", + } + else: + return { + "success": False, + "status": "Closed/Filtered", + "latency_ms": None, + "details": f"No se pudo conectar (código: {result})", + } + except socket.timeout: + return { + "success": False, + "status": "Timeout", + "latency_ms": None, + "details": "Timeout esperando respuesta", + } + except Exception as e: + return { + "success": False, + "status": "Error", + "latency_ms": None, + "error": str(e), + "details": f"Error: {str(e)}", + } + + def _test_udp(self, host: str, port: int) -> Dict: + """Test básico UDP (envía paquete y espera respuesta).""" + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(3) + start_time = time.time() + + # Enviar paquete vacío + sock.sendto(b"", (host, port)) + + try: + data, addr = sock.recvfrom(1024) + latency = (time.time() - start_time) * 1000 + sock.close() + return { + "success": True, + "status": "Open", + "latency_ms": round(latency, 2), + "details": f"Respuesta UDP recibida en {latency:.2f}ms", + } + except socket.timeout: + sock.close() + return { + "success": False, + "status": "Open|Filtered", + "latency_ms": None, + "details": "No respuesta (puerto puede estar abierto pero filtrado)", + } + except Exception as e: + return { + "success": False, + "status": "Error", + "latency_ms": None, + "error": str(e), + "details": f"Error: {str(e)}", + } + + def _test_ntp(self, host: str) -> Dict: + """Test NTP específico (puerto UDP 123).""" + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(5) + start_time = time.time() + + # Paquete NTP básico (modo 3: cliente) + ntp_packet = bytearray(48) + ntp_packet[0] = 0x1B # LI=0, VN=3, Mode=3 + + sock.sendto(ntp_packet, (host, 123)) + + try: + data, addr = sock.recvfrom(1024) + latency = (time.time() - start_time) * 1000 + + if len(data) >= 48: + # Extraer timestamp NTP + try: + seconds = ( + (data[40] << 24) + | (data[41] << 16) + | (data[42] << 8) + | data[43] + ) + # NTP epoch: 1900-01-01 + ntp_epoch = datetime(1900, 1, 1) + ntp_time = ntp_epoch.timestamp() + seconds + ntp_dt = datetime.fromtimestamp(ntp_time) + time_str = ntp_dt.strftime('%Y-%m-%d %H:%M:%S') + except (ValueError, OverflowError, OSError): + time_str = "Timestamp no válido" + + sock.close() + return { + "success": True, + "status": "Open", + "latency_ms": round(latency, 2), + "details": f"NTP hora local: {time_str}", + } + else: + sock.close() + return { + "success": False, + "status": "Error", + "latency_ms": None, + "details": "Respuesta NTP incompleta", + } + except socket.timeout: + sock.close() + return { + "success": False, + "status": "Timeout", + "latency_ms": None, + "details": "Timeout esperando respuesta NTP", + } + except Exception as e: + return { + "success": False, + "status": "Error", + "latency_ms": None, + "error": str(e), + "details": f"Error NTP: {str(e)}", + } + + +class VLAN35Checker: + """Gestor de tests de conectividad para VLAN 35.""" + + def __init__(self): + # Configuración por defecto basada en el script .bat + self.default_tests = [ + {"host": "10.47.8.133", "port": 515, "protocol": "TCP", "type": "connectivity"}, + {"host": "10.47.8.133", "port": 9195, "protocol": "TCP", "type": "connectivity"}, + {"host": "10.47.8.135", "port": 9191, "protocol": "TCP", "type": "connectivity"}, + {"host": "10.47.8.135", "port": 9192, "protocol": "TCP", "type": "connectivity"}, + {"host": "10.240.225.171", "port": 465, "protocol": "TCP", "type": "connectivity"}, + {"host": "10.244.255.250", "port": 123, "protocol": "UDP", "type": "ntp"}, + ] + + def get_default_tests(self) -> List[Dict]: + """Retorna la lista de tests por defecto.""" + return self.default_tests.copy() + + def run_tests(self, tests: List[Dict]) -> ConnectivityTestThread: + """Inicia los tests en un thread.""" + thread = ConnectivityTestThread(tests) + thread.start() + return thread diff --git a/src/gui/__pycache__/main_window.cpython-312.pyc b/src/gui/__pycache__/main_window.cpython-312.pyc index 941de8759856d32ceb3b657061e433f46149e163..3ec6bef2dfbc64664cd415162335e9dd0112db88 100644 GIT binary patch delta 4726 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

-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 I4pD=D0erFft^fc4 delta 982 zcmZuv%}*0i5Z{IUYL~8|-R+hNbZNn$*l#Fb35iNL7!?Tz6-Z#yF0e{>%eGC>n1G2F z6TiqDBqk(YjENc(1f^k(9F+m?fICi%UYH}htGZ{GVka3?9e zbGe)ZlAYmzz9_B=gL1t_&emn9+tdV|Au{v=lVvAZl31MyZ@><{d?R)$ms`-ayrL>O zOUvrTtddc!Yf52W%W6u&#D=2coU~fdO(RlbMv8f4?6smkdm@G(2&_76En(Qka2G=v z!!iPOS|A9gQkAjus%~7;jf@GSY&-OO{BV!$gOBVG71LrGEPB0gf{Vjzw+JTZViG+_ zQ?M&XVS^i?wK!6k<|Q`NZ_B37p^&vKv=8IX2-T~m)1W#eP(+cn3k)i-&x^3d@$lIh zqB9AkxC}~f1YY1F%vX<1^Cc>kP?JCk654gbRKv;o?sT8c4Erw{x@>k?o`wB}ez+wC zZ9nJoFx|FP_|3+!!gEgqwq0J-WB?|G_6oN)UmKH5W3}5fxg*t^tsV)&rrQHK7Xx2J zFRZ&qm_(|3wzm)V+@rO7Vrt`_o|_x+}K~rE*JyhwqD@IjdT8x)Cmzxd9~+ z`!||XGa22OtAyh{@Sg8MRXKRzy^EGNr)o3Zl~pEbi#@VeG7s~E`Gao&B;Y@futs3yy z-yu-wujU2xl~;)G6boDaVRz`0=h(`*ub!s0wzcsm*xT$lx=sc-g66g<=>sK|iDgpe zej%hC2dDsXoU~e@)SQ&8DDp}T7q5>MQjxL38Pn2GB-j$_44^={o+Qa{MCT6CS*E7z UNXNs40|KwVPtye1Y{gK{Uy+Oxvj6}9 diff --git a/src/gui/main_window.py b/src/gui/main_window.py index 01576c8..7c349a6 100644 --- a/src/gui/main_window.py +++ b/src/gui/main_window.py @@ -1,5 +1,13 @@ -from PyQt6.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QTabWidget, - QLabel, QHBoxLayout, QStatusBar) +from PyQt6.QtWidgets import ( + QMainWindow, + QWidget, + QVBoxLayout, + QTabWidget, + QLabel, + QHBoxLayout, + QStatusBar, + QFrame, +) from PyQt6.QtCore import Qt from src.core.network_manager import NetworkManager from src.gui.widgets.interface_list import InterfaceListWidget @@ -9,65 +17,118 @@ from src.gui.widgets.scanner_view import ScannerWidget from src.gui.widgets.sniffer_view import SnifferWidget from src.gui.widgets.diagnostics_view import DiagnosticsWidget from src.gui.widgets.port_scanner import PortScannerWidget +from src.gui.widgets.advanced_routing import AdvancedRoutingWidget +from src.gui.widgets.network_monitor import NetworkMonitorWidget +from src.gui.widgets.vlan35_check import VLAN35CheckWidget from src.utils.report_generator import ReportGenerator from PyQt6.QtWidgets import QFileDialog class MainWindow(QMainWindow): def __init__(self): super().__init__() - self.setWindowTitle("JNC-NetTools // ADA version") - self.setMinimumSize(1000, 700) # Allow resizing, set minimum size + self.setWindowTitle("JNC-NetTools · ADA - Agencia Digital de Andalucía") + self.setMinimumSize(1100, 720) # Tamaño mínimo algo más amplio self.nm = NetworkManager() - # Central Widget + # Central Widget y layout principal vertical central_widget = QWidget() self.setCentralWidget(central_widget) - main_layout = QHBoxLayout(central_widget) - - # Sidebar (Interface List) + outer_layout = QVBoxLayout(central_widget) + outer_layout.setContentsMargins(14, 10, 14, 10) + outer_layout.setSpacing(10) + + # Encabezado corporativo superior + header = QFrame() + header.setObjectName("HeaderBar") + header_layout = QHBoxLayout(header) + header_layout.setContentsMargins(12, 8, 12, 8) + + title_block = QVBoxLayout() + title_label = QLabel("JNC-NetTools") + title_label.setObjectName("HeaderTitle") + subtitle_label = QLabel("Suite de diagnóstico y exploración de red · ADA") + subtitle_label.setObjectName("HeaderSubtitle") + subtitle_label.setAlignment(Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft) + title_block.addWidget(title_label) + title_block.addWidget(subtitle_label) + + header_layout.addLayout(title_block) + header_layout.addStretch() + + org_label = QLabel("Agencia Digital de Andalucía · Junta de Andalucía") + org_label.setObjectName("HeaderOrg") + header_layout.addWidget(org_label) + + outer_layout.addWidget(header) + + # Separador visual + separator = QFrame() + separator.setFrameShape(QFrame.Shape.HLine) + separator.setFrameShadow(QFrame.Shadow.Sunken) + outer_layout.addWidget(separator) + + # Área de contenido principal (sidebar + pestañas) + main_layout = QHBoxLayout() + main_layout.setSpacing(12) + outer_layout.addLayout(main_layout) + + # Sidebar (lista de interfaces) self.interface_list = InterfaceListWidget(self.nm) self.interface_list.setFixedWidth(300) self.interface_list.interface_selected.connect(self.on_interface_selected) main_layout.addWidget(self.interface_list) - # Main Content Area (Tabs) + # Área principal de pestañas self.tabs = QTabWidget() main_layout.addWidget(self.tabs) - # IP Configuration Tab + # Pestaña Configuración IP self.ip_config_widget = IPConfigWidget(self.nm) - self.tabs.addTab(self.ip_config_widget, "IP Configuration") + self.tabs.addTab(self.ip_config_widget, "Configuración IP") - # VLAN & Bridge Tab + # Pestaña VLAN & Bridge self.vlan_bridge_widget = VLANBridgeWidget(self.nm) - self.tabs.addTab(self.vlan_bridge_widget, "VLAN / Bridge") + self.tabs.addTab(self.vlan_bridge_widget, "VLAN / Puente") - # Scanner Tab (Network Discovery) + # Pestaña de escaneo de red self.scanner_widget = ScannerWidget() - self.tabs.addTab(self.scanner_widget, "Net Scanner") + self.tabs.addTab(self.scanner_widget, "Exploración de Red") - # Port Scanner Tab (Dedicated) + # Pestaña de escáner de puertos self.port_scanner_widget = PortScannerWidget() - self.tabs.addTab(self.port_scanner_widget, "Port Scanner") + self.tabs.addTab(self.port_scanner_widget, "Escáner de Puertos") - # Sniffer Tab + # Pestaña Sniffer self.sniffer_widget = SnifferWidget(self.nm) - self.tabs.addTab(self.sniffer_widget, "Sniffer") + self.tabs.addTab(self.sniffer_widget, "Sniffer de Tráfico") - # Diagnostics Tab + # Pestaña Diagnóstico self.diagnostics_widget = DiagnosticsWidget() - self.tabs.addTab(self.diagnostics_widget, "Diagnostics") + self.tabs.addTab(self.diagnostics_widget, "Diagnóstico") + + # Pestaña Monitor de Red + self.network_monitor_widget = NetworkMonitorWidget(self.nm) + self.tabs.addTab(self.network_monitor_widget, "Monitor de Red") + + # Pestaña Routing Avanzado + self.advanced_routing_widget = AdvancedRoutingWidget(self.nm) + self.tabs.addTab(self.advanced_routing_widget, "Routing Avanzado") + + # Pestaña Comprobación VLAN35 + self.vlan35_check_widget = VLAN35CheckWidget() + self.tabs.addTab(self.vlan35_check_widget, "Comprobación VLAN35") - # Report Button (Top Right or Bottom) - # Let's add a toolbar for global actions - toolbar = self.addToolBar("Main") - toolbar.addAction("Generate Report", self.generate_report) + # Barra de herramientas para acciones globales + toolbar = self.addToolBar("Principal") + toolbar.setMovable(False) + toolbar.setFloatable(False) + toolbar.addAction("Generar informe HTML", self.generate_report) - # Status Bar + # Barra de estado self.status_bar = QStatusBar() self.setStatusBar(self.status_bar) - self.status_bar.showMessage("Ready") + self.status_bar.showMessage("Listo") def on_interface_selected(self, iface_data): self.status_bar.showMessage(f"Selected Interface: {iface_data['name']}") @@ -78,6 +139,7 @@ class MainWindow(QMainWindow): self.scanner_widget.close() self.port_scanner_widget.close() self.vlan_bridge_widget.close() + # advanced_routing_widget no mantiene hilos propios, sólo estado lógico event.accept() def generate_report(self): diff --git a/src/gui/styles.py b/src/gui/styles.py index e44c519..f839b9e 100644 --- a/src/gui/styles.py +++ b/src/gui/styles.py @@ -1,20 +1,25 @@ from PyQt6.QtWidgets import QApplication from PyQt6.QtGui import QPalette, QColor +from PyQt6.QtCore import Qt + def apply_styles(app: QApplication): - """Applies a premium dark theme to the application.""" + """ + Aplica un tema oscuro corporativo inspirado en la identidad visual + de la Agencia Digital de Andalucía / Junta de Andalucía. + """ app.setStyle("Fusion") - + palette = QPalette() - - # Colors - dark_bg = QColor(30, 30, 30) - darker_bg = QColor(20, 20, 20) - text_color = QColor(230, 230, 230) - accent_color = QColor(0, 120, 215) # A nice blue - accent_hover = QColor(0, 140, 240) - disabled_text = QColor(127, 127, 127) - + + # Colores corporativos aproximados ADA / Junta de Andalucía + dark_bg = QColor(18, 18, 18) + darker_bg = QColor(10, 10, 10) + text_color = QColor(235, 235, 235) + accent_color = QColor(0, 133, 66) # Verde principal corporativo + accent_hover = QColor(0, 153, 80) + disabled_text = QColor(130, 130, 130) + palette.setColor(QPalette.ColorRole.Window, dark_bg) palette.setColor(QPalette.ColorRole.WindowText, text_color) palette.setColor(QPalette.ColorRole.Base, darker_bg) @@ -28,29 +33,28 @@ def apply_styles(app: QApplication): palette.setColor(QPalette.ColorRole.Link, accent_color) palette.setColor(QPalette.ColorRole.Highlight, accent_color) palette.setColor(QPalette.ColorRole.HighlightedText, Qt.GlobalColor.black) - + app.setPalette(palette) - - # Stylesheet for specific widgets to add that "premium" feel - # Stylesheet for specific widgets to add that "premium" feel + + # Stylesheet para dar un aspecto más “suite corporativa” app.setStyleSheet(""" QMainWindow { - background-color: #181818; + background-color: #101010; } QWidget { - font-family: 'Segoe UI', 'Roboto', sans-serif; + font-family: 'Segoe UI', 'Roboto', 'Noto Sans', sans-serif; font-size: 13px; - color: #e0e0e0; + color: #e5e5e5; } QTabWidget::pane { - border: 1px solid #333333; - background: #252526; + border: 1px solid #2b2b2b; + background: #181818; border-radius: 4px; top: -1px; } QTabBar::tab { - background: #1e1e1e; - color: #999999; + background: #151515; + color: #aaaaaa; padding: 10px 20px; border-top-left-radius: 4px; border-top-right-radius: 4px; @@ -59,17 +63,17 @@ def apply_styles(app: QApplication): border: 1px solid transparent; } QTabBar::tab:selected { - background: #252526; + background: #181818; color: #ffffff; - border-top: 2px solid #007acc; - border-bottom: 1px solid #252526; + border-top: 2px solid #008542; + border-bottom: 1px solid #181818; } QTabBar::tab:hover { - background: #2d2d2d; + background: #202020; color: #ffffff; } QPushButton { - background-color: #007acc; + background-color: #008542; color: white; border: none; padding: 8px 16px; @@ -77,89 +81,110 @@ def apply_styles(app: QApplication): font-weight: 600; } QPushButton:hover { - background-color: #0062a3; + background-color: #009b4f; } QPushButton:pressed { - background-color: #004c80; + background-color: #006f39; } QPushButton:disabled { - background-color: #333333; + background-color: #2a2a2a; color: #666666; } - QLineEdit, QComboBox, QSpinBox { - background-color: #333333; - border: 1px solid #444444; - color: #f0f0f0; - padding: 6px; + QLineEdit, QComboBox, QSpinBox, QDoubleSpinBox { + background-color: #232323; + border: 1px solid #3a3a3a; + color: #f5f5f5; + padding: 4px 6px; border-radius: 3px; + min-height: 22px; } - QLineEdit:focus, QComboBox:focus, QSpinBox:focus { - border: 1px solid #007acc; - background-color: #3c3c3c; + QLineEdit:focus, QComboBox:focus, QSpinBox:focus, QDoubleSpinBox:focus { + border: 1px solid #008542; + background-color: #272727; + } + /* Botones de los spinbox: que se vean siempre claros */ + QSpinBox::up-button, QDoubleSpinBox::up-button, + QSpinBox::down-button, QDoubleSpinBox::down-button { + background-color: #303030; + border: 1px solid #3a3a3a; + width: 16px; + } + QSpinBox::up-button:hover, QDoubleSpinBox::up-button:hover, + QSpinBox::down-button:hover, QDoubleSpinBox::down-button:hover { + background-color: #3a3a3a; + } + QSpinBox::up-arrow, QDoubleSpinBox::up-arrow, + QSpinBox::down-arrow, QDoubleSpinBox::down-arrow { + width: 8px; + height: 8px; + } + /* Formularios más compactos */ + QFormLayout QLabel { + min-width: 110px; + } + QGroupBox { + margin-top: 12px; + padding-top: 10px; } QTableWidget { - background-color: #1e1e1e; - border: 1px solid #333333; + background-color: #141414; + border: 1px solid #2b2b2b; border-radius: 4px; gridline-color: #333333; - selection-background-color: #264f78; + selection-background-color: #184b33; selection-color: white; - alternate-background-color: #252526; + alternate-background-color: #181818; } QHeaderView::section { - background-color: #252526; - color: #cccccc; + background-color: #181818; + color: #dddddd; padding: 6px; border: none; - border-right: 1px solid #333333; - border-bottom: 1px solid #333333; + border-right: 1px solid #2b2b2b; + border-bottom: 1px solid #2b2b2b; font-weight: 600; } QListWidget { - background-color: #1e1e1e; - border: 1px solid #333333; + background-color: #141414; + border: 1px solid #2b2b2b; border-radius: 4px; } QListWidget::item { padding: 8px; - border-bottom: 1px solid #252526; + border-bottom: 1px solid #202020; } QListWidget::item:selected { - background-color: #37373d; + background-color: #184b33; color: white; - border-left: 2px solid #007acc; + border-left: 2px solid #00a357; } QGroupBox { - border: 1px solid #444444; + border: 1px solid #3a3a3a; border-radius: 6px; - margin-top: 20px; - padding-top: 15px; font-weight: 600; } QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; padding: 0 5px; - color: #007acc; + color: #00a357; left: 10px; } QStatusBar { - background-color: #007acc; + background-color: #101010; color: white; } QProgressBar { - border: 1px solid #444444; + border: 1px solid #3a3a3a; border-radius: 4px; text-align: center; - background-color: #333333; + background-color: #232323; } QProgressBar::chunk { - background-color: #007acc; + background-color: #00a357; width: 10px; } QSplitter::handle { - background-color: #333333; + background-color: #2b2b2b; } """) - -from PyQt6.QtCore import Qt diff --git a/src/gui/widgets/__pycache__/advanced_routing.cpython-312.pyc b/src/gui/widgets/__pycache__/advanced_routing.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fa6fb95cdf4e5410b9e8e7e15c3519345bb11a2a GIT binary patch literal 41521 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 bbJhPaZTN4d4WBs1T;|Z+flp2NOB((^kc&LP literal 0 HcmV?d00001 diff --git a/src/gui/widgets/__pycache__/diagnostics_view.cpython-312.pyc b/src/gui/widgets/__pycache__/diagnostics_view.cpython-312.pyc index f494d625a9209a91d9c5f9c1eb4ac175f0a71dda..d905f33d136dbea3c6ad25a025072f7f22f34590 100644 GIT binary patch delta 74 zcmbOpvowbLG%qg~0}w>^MQ-HoVloKU&&bbB)z8XLEG*5;(|5^FF3nBNE6Fd`4=Bn{ cFG|c!EY|n(bJq1sEeXlb&ne!#ohd;Z0Eb2yi2wiq delta 60 zcmZ1)Gd+g;G%qg~0}x2a$ZzECVp4L|&&bbB)z8XLEG*5;(|5^FF3nBNE6Fd`_wsYr O4NgwX+kBKMK^p)O$`b$p diff --git a/src/gui/widgets/__pycache__/interface_list.cpython-312.pyc b/src/gui/widgets/__pycache__/interface_list.cpython-312.pyc index 7e3cc3754adf589b11a695e5edd547abc1af35e0..c826dcb50df73f34bf5563ebdbf3646d0266707e 100644 GIT binary patch delta 3029 zcma)7U2Gf25x(W`i8}ttB5hf+bh0T@lp~5(Y}xv;<4CgAIJPR8PE*qklrwKh9ZlXT zdv~!&$Y6j5t%3$m+% zh?ZPqo=b9hchYV2tvOHLoAl;=NnhTd^aIAiOfaJDRYtT!uxzES7$(_Na73M-P&Mkh z@pjr>Yn(m@RaMBqQDq)m!AWoukAovV3QRUEGV>tmN-$A7{uVfB5$!9tKn&PK+cLoS zEhEH$e`oCko%jn^2mX^acyK@Uy#32|ZST|G0+S76*}511(b^J-etBGU;zdg*)@>cQ zVhOkd<_v=C!GE!a!2gi9j34C(+t1NE5Ttu{Nj zrJI`AAT}&p>bkYwpNlAUFo>J1sunJ3$&L+GCb2^#?_uSwdDVt{Z}R$Fw0Q1{kh+>diY|-2 zDJ7?%A^t!*J$Ni0ALcju*JdRR4)b$@C`xi>h(A0x&mZcan;+)WimdfsgHl&!RvY36 z2;u4$dmORn8psT&&0Nvsnu{nG6eV{-nya~q zp3n+8n3#o7t2t9SDRmW!L{VfJrnH(*k!R{qbBMxE$-{M@tRlRmOLE`1lG5{Js-pHyAti%^ zyrA}-nH=k#tS71Vo>N5pUFQL?7r*NagMZ<_I_eJ1-l2pEC zPvxM1P%HkeZ#2G-SdgCzx+{ntY!o7*X3db2+x59Qh!9V$^i-omc+fur-1udGI~c$} z@PFhOrsg;D@*GLv7|QcB2gKJvKRe7Sj9@~B^aX^he?TkNY`(ew+nTTX2q`u+WHq55r`I({{d65?hFuZEyTlhKwwLO zwitogMaKeoixHh~TeZ#2$gcp>08n1QT@Yvr&R6EO_eZsMn?)SLjEP4{v}9g(40$9kLMB5FA-?u zDVK|YX>@ewCa=r9!TSY1r^AdwxSuZ(Z;#<({8@oez6|H3J1a7YSkD>h`8kPtxPsuz zkdh;e6?birdvP-bQHXN&+eS}A&3R2gGI=d(EXh~Z$kS8vBs4)51tgB@nnwE8(nYA& zERzaqr3i1G*Eim*6s$E$a#|rJpzTU)e z{_j6qZH|?jW7X#Va&!Otjs0t_9X~nxw-XiLGxvSXb<;gx^kX_qGhMpyPSep_WA}W=*1Zh(q(NQLM*Lo|i>LRF zFshW&kW5sxmkPQG=m-@N{GVVTF+^0AQr*cqbMB%;R5<7WeN?`9mHD9Mz|#2LmguT2 z_|O)%@BefP4B3Z1od-7iq0hM%`=AllfK5hsVPW@)Zy@6caZ`n9Z6G$SjVTD$dELmW z(X>zK67tfJJ}QhSjRyY(Q};;8{1H2nS42GrPoPD-zB|x!8T^|8z?A=BA|EkNl$j?U R+D==*;Ey7oF|;-C{{L@!3A7IERu#875t7U7C+L1TG>@gwic#a1B7Ta z9yIZS?_xY@yl6-?k%Nf`PbMBbC^5!ZjUI_7KLVnjoLMAs4!`}+dvE^l&Fs_dk2Z#0 z27`W%u1~wQ;_vWeC;>y0O>Jr5OwQzOe<_d;l$5;U?mc#}6v~H6;e5DMm#>?=xI$Sr zCh6WU%s}@|-aUgc*alfV0vZ1`ZWQvq9H$F-8(Mfyn0^5U;C>;1yr)B$;D%P?D?W** z!ZF23GY7VY8gYb=!g_opCUHRw;XEk0BqLL|^;uMc%j|K)ijQ52o zv)@ELGME-tAxJAAb2Ac0L-MV0wk-Kf{Sy=1$(2hndalczAp5^nH-42gd@eOZ7gNxI zVXp=fwt3U|(VGMf7rdQNhlXt6WBIV^uUO7Nxoi&7w5RP%3jAR z^%k7M;j(SjAG6xJ`XF-?%;KO!&QVDc49yG%Uee+RI;gAw z#kNKN2um?YEWw`R*H!Mly7o@*8&!WTET0qV#Wg>&Suy)_6nL@S?d2Zm(<3;mZM{@e z4vqCYokqXYQzq8p9o;DcOYz;+A_=iRVFvf-u#OM7MKgBKKXP}eY*uY+D{<(S4OlTK nzIgy(fd@DU3moIT%UQ1JGuJx9way94P5`vuYMSTR*EReL7(L_L diff --git a/src/gui/widgets/__pycache__/ip_config.cpython-312.pyc b/src/gui/widgets/__pycache__/ip_config.cpython-312.pyc index da359fb67a15040d42699b7f1dc1673b71cda5be..b8c3ad37879983a96582802ddedb0cef53607621 100644 GIT binary patch delta 74 zcmZ4Cv(1P5G%qg~0}w>^MQ-Fi&1c}FpOK%Ns-KmgSXi2wr|*)VT$-DjSCU_>A5fH^ cUX+-dSgh~m=dA0OS`w0@8##L O8=Rb&x0#LKKpX%f$r645 diff --git a/src/gui/widgets/__pycache__/network_monitor.cpython-312.pyc b/src/gui/widgets/__pycache__/network_monitor.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..485b40e58aee20be751abfd3d03bf8dcd6ff05c6 GIT binary patch literal 8382 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 literal 0 HcmV?d00001 diff --git a/src/gui/widgets/__pycache__/port_scanner.cpython-312.pyc b/src/gui/widgets/__pycache__/port_scanner.cpython-312.pyc index acdaa049584f9903bfed53915c6803eeab217890..d3a8e9447125b87c14bb58abc6f0175e815d63b3 100644 GIT binary patch delta 76 zcmX>*nep~yM()$Ryj%=G5ZM>Gkvp5;z+XQjKQ~oBD?hQYG&4`%B|o_|H#M&$zgRz@ eC_lX@F*mVT-^^MQ-Gt!ENBLpOK%Ns-KmgSXi2wr|*)VT$-DjSCU_>A5fH^ cUX+-dSgh~m=dA0OS`w0@8##L O8=Rb&xA`*nGZ_Fb(G(*9 diff --git a/src/gui/widgets/__pycache__/sniffer_view.cpython-312.pyc b/src/gui/widgets/__pycache__/sniffer_view.cpython-312.pyc index 7eee041fbd2813506cdd8d4f00e6d89b8d52f8da..88cff47a8d328ea7c491beb8df877383120b8eba 100644 GIT binary patch delta 54 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 eQGR++Vs2uwzL%e~u3u_NNPd1!@n#9N?LGh$g&T access + if src_vlan is None: + QMessageBox.warning( + self, + "Error", + "Para Tagged → Untagged la VLAN de origen no puede ser 0.", + ) + return + ok, msg = self.router.redirect_vlan_tagged_to_untagged( + trunk_iface=src_iface, vlan_id=src_vlan, access_iface=dst_iface + ) + else: + ok, msg = self.router.redirect_vlan_between_interfaces( + src_iface=src_iface, + src_vlan_id=src_vlan, + dst_iface=dst_iface, + dst_vlan_id=dst_vlan, + ) + + if ok: + QMessageBox.information(self, "Redirección", msg) + self.refresh_redirection_table() + else: + QMessageBox.critical(self, "Error redirección", msg) + + def refresh_redirection_table(self): + status = self.router.get_routing_status() + rules = status.get("vlan_redirections", []) + self.redir_table.setRowCount(len(rules)) + for row, r in enumerate(rules): + self.redir_table.setItem(row, 0, QTableWidgetItem(r.get("src_iface", ""))) + self.redir_table.setItem( + row, + 1, + QTableWidgetItem(str(r.get("src_vlan", "") or "untagged")), + ) + self.redir_table.setItem(row, 2, QTableWidgetItem(r.get("dst_iface", ""))) + self.redir_table.setItem( + row, + 3, + QTableWidgetItem(str(r.get("dst_vlan", "") or "untagged")), + ) + self.redir_table.setItem(row, 4, QTableWidgetItem(r.get("bridge", ""))) + + # ------------------------------------------------------------------ + # Redes (Gateway / DHCP) + # ------------------------------------------------------------------ + + def _init_networks_tab(self): + tab = QWidget() + layout = QVBoxLayout(tab) + + group = QGroupBox("Configuración de red por VLAN / interfaz") + form = QFormLayout(group) + + self.net_iface_combo = QComboBox() + self.net_ip_input = QLineEdit() + self.net_gw_input = QLineEdit() + self.net_dns_input = QLineEdit() + self.net_dhcp_cb = QCheckBox("Habilitar DHCP") + self.net_dhcp_start = QLineEdit() + self.net_dhcp_end = QLineEdit() + + apply_btn = QPushButton("Aplicar configuración") + apply_btn.clicked.connect(self.on_apply_network_config_clicked) + + form.addRow("Interfaz / VLAN:", self.net_iface_combo) + form.addRow("IP Gateway:", self.net_ip_input) + form.addRow("Gateway (next-hop):", self.net_gw_input) + form.addRow("DNS:", self.net_dns_input) + form.addRow(self.net_dhcp_cb) + form.addRow("DHCP Inicio:", self.net_dhcp_start) + form.addRow("DHCP Fin:", self.net_dhcp_end) + form.addRow("", apply_btn) + + layout.addWidget(group) + + self.net_table = QTableWidget() + self.net_table.setColumnCount(6) + self.net_table.setHorizontalHeaderLabels( + ["Interfaz", "IP", "Gateway", "DHCP", "Rango", "DNS"] + ) + self.net_table.horizontalHeader().setSectionResizeMode( + QHeaderView.ResizeMode.Stretch + ) + layout.addWidget(self.net_table) + + self.tabs.addTab(tab, "Redes") + + def on_apply_network_config_clicked(self): + iface = self.net_iface_combo.currentText() + ip = self.net_ip_input.text().strip() + gw = self.net_gw_input.text().strip() + dns = self.net_dns_input.text().strip() or None + dhcp = self.net_dhcp_cb.isChecked() + dhcp_start = self.net_dhcp_start.text().strip() + dhcp_end = self.net_dhcp_end.text().strip() + + if not iface or not ip: + QMessageBox.warning( + self, "Error", "Interfaz y IP son obligatorios para la red." + ) + return + + if gw: + ok, msg = self.router.set_gateway_for_network(iface, ip, gw) + if not ok: + QMessageBox.critical(self, "Error gateway", msg) + return + + if dhcp: + if not (dhcp_start and dhcp_end): + QMessageBox.warning( + self, + "Error", + "Para DHCP debes indicar rango de inicio y fin.", + ) + return + ok, msg = self.router.start_dhcp_server_for_vlan( + vlan_interface=iface, + ip=ip, + range_start=dhcp_start, + range_end=dhcp_end, + gateway=gw or None, + dns=dns, + ) + if not ok: + QMessageBox.critical(self, "Error DHCP", msg) + return + + QMessageBox.information(self, "Red", "Configuración aplicada.") + self.refresh_networks_table() + + def refresh_networks_table(self): + status = self.router.get_routing_status() + gws = status.get("gateways", {}) + dhcps = status.get("dhcp_servers", {}) + self.net_table.setRowCount(len(gws)) + for row, (iface, data) in enumerate(gws.items()): + ip = data.get("ip", "") + gw = data.get("gateway", "") + dns = data.get("dns", "") + dh = dhcps.get(iface) + dh_enabled = "Sí" if dh else "No" + dh_range = "" + if dh: + dh_range = f"{dh.get('range_start','')} - {dh.get('range_end','')}" + + self.net_table.setItem(row, 0, QTableWidgetItem(iface)) + self.net_table.setItem(row, 1, QTableWidgetItem(ip)) + self.net_table.setItem(row, 2, QTableWidgetItem(gw)) + self.net_table.setItem(row, 3, QTableWidgetItem(dh_enabled)) + self.net_table.setItem(row, 4, QTableWidgetItem(dh_range)) + self.net_table.setItem(row, 5, QTableWidgetItem(dns)) + + # ------------------------------------------------------------------ + # Routing (rutas + reglas) + # ------------------------------------------------------------------ + + def _init_routing_tab(self): + tab = QWidget() + layout = QVBoxLayout(tab) + + group = QGroupBox("Rutas estáticas") + form = QFormLayout(group) + + self.route_dst_input = QLineEdit() + self.route_dst_input.setPlaceholderText("Ej: 10.0.0.0/24") + self.route_gw_input = QLineEdit() + self.route_iface_combo = QComboBox() + self.route_table_spin = QSpinBox() + self.route_table_spin.setRange(1, 252) + self.route_table_spin.setValue(254) + self.route_metric_spin = QSpinBox() + self.route_metric_spin.setRange(1, 1000) + self.route_metric_spin.setValue(100) + + add_btn = QPushButton("Añadir ruta") + add_btn.clicked.connect(self.on_add_route_clicked) + + form.addRow("Destino:", self.route_dst_input) + form.addRow("Gateway:", self.route_gw_input) + form.addRow("Interfaz:", self.route_iface_combo) + form.addRow("Tabla:", self.route_table_spin) + form.addRow("Métrica:", self.route_metric_spin) + form.addRow("", add_btn) + + layout.addWidget(group) + + self.routes_table = QTableWidget() + self.routes_table.setColumnCount(5) + self.routes_table.setHorizontalHeaderLabels( + ["Destino", "Gateway", "Interfaz", "Tabla", "Métrica"] + ) + self.routes_table.horizontalHeader().setSectionResizeMode( + QHeaderView.ResizeMode.Stretch + ) + layout.addWidget(self.routes_table) + + fw_group = QGroupBox("Reglas de routing (policy-based)") + fw_form = QFormLayout(fw_group) + + self.rule_table_spin = QSpinBox() + self.rule_table_spin.setRange(1, 252) + self.rule_table_spin.setValue(100) + self.rule_prio_spin = QSpinBox() + self.rule_prio_spin.setRange(1, 32765) + self.rule_prio_spin.setValue(32000) + self.rule_src_input = QLineEdit() + self.rule_dst_input = QLineEdit() + self.rule_iif_input = QLineEdit() + self.rule_oif_input = QLineEdit() + add_rule_btn = QPushButton("Añadir regla") + add_rule_btn.clicked.connect(self.on_add_rule_clicked) + + fw_form.addRow("Tabla:", self.rule_table_spin) + fw_form.addRow("Prioridad:", self.rule_prio_spin) + fw_form.addRow("Origen (CIDR):", self.rule_src_input) + fw_form.addRow("Destino (CIDR):", self.rule_dst_input) + fw_form.addRow("Interfaz entrada:", self.rule_iif_input) + fw_form.addRow("Interfaz salida:", self.rule_oif_input) + fw_form.addRow("", add_rule_btn) + + layout.addWidget(fw_group) + + enable_fwd_btn = QPushButton("Habilitar IP forwarding") + enable_fwd_btn.clicked.connect(self.on_enable_forwarding_clicked) + layout.addWidget(enable_fwd_btn) + + self.tabs.addTab(tab, "Routing") + + def on_add_route_clicked(self): + dst = self.route_dst_input.text().strip() + gw = self.route_gw_input.text().strip() + iface = self.route_iface_combo.currentText() + table = self.route_table_spin.value() + metric = self.route_metric_spin.value() + + if not (dst and gw and iface): + QMessageBox.warning( + self, "Error", "Destino, gateway e interfaz son obligatorios." + ) + return + + ok, msg = self.router.add_route( + dst=dst, gateway=gw, interface=iface, table=table, metric=metric + ) + if ok: + QMessageBox.information(self, "Ruta", msg) + self.refresh_routes_table() + else: + QMessageBox.critical(self, "Error ruta", msg) + + def on_add_rule_clicked(self): + table = self.rule_table_spin.value() + prio = self.rule_prio_spin.value() + src = self.rule_src_input.text().strip() or None + dst = self.rule_dst_input.text().strip() or None + iif = self.rule_iif_input.text().strip() or None + oif = self.rule_oif_input.text().strip() or None + + ok, msg = self.router.add_routing_rule( + table=table, priority=prio, src=src, dst=dst, iifname=iif, oifname=oif + ) + if ok: + QMessageBox.information(self, "Regla", msg) + else: + QMessageBox.critical(self, "Error regla", msg) + + def on_enable_forwarding_clicked(self): + ok, msg = self.router.enable_ip_forwarding() + if ok: + QMessageBox.information(self, "IP forwarding", msg) + else: + QMessageBox.critical(self, "Error", msg) + + def refresh_routes_table(self): + routes = self.router.list_routes() + self.routes_table.setRowCount(len(routes)) + for row, r in enumerate(routes): + self.routes_table.setItem(row, 0, QTableWidgetItem(r.get("dst", ""))) + self.routes_table.setItem(row, 1, QTableWidgetItem(r.get("gateway", ""))) + self.routes_table.setItem(row, 2, QTableWidgetItem(r.get("interface", ""))) + self.routes_table.setItem( + row, 3, QTableWidgetItem(str(r.get("table", ""))) + ) + self.routes_table.setItem( + row, 4, QTableWidgetItem(str(r.get("metric", ""))) + ) + + # ------------------------------------------------------------------ + # NAT + # ------------------------------------------------------------------ + + def _init_nat_tab(self): + tab = QWidget() + layout = QVBoxLayout(tab) + + group = QGroupBox("NAT y Port Forwarding") + form = QFormLayout(group) + + self.nat_internal_iface_combo = QComboBox() + self.nat_external_iface_combo = QComboBox() + enable_nat_btn = QPushButton("Habilitar NAT") + enable_nat_btn.clicked.connect(self.on_enable_nat_clicked) + + self.pf_ext_ip = QLineEdit() + self.pf_ext_port = QSpinBox() + self.pf_ext_port.setRange(1, 65535) + self.pf_int_ip = QLineEdit() + self.pf_int_port = QSpinBox() + self.pf_int_port.setRange(1, 65535) + self.pf_proto_combo = QComboBox() + self.pf_proto_combo.addItems(["tcp", "udp"]) + add_pf_btn = QPushButton("Añadir port forwarding") + add_pf_btn.clicked.connect(self.on_add_pf_clicked) + + form.addRow("Interfaz interna:", self.nat_internal_iface_combo) + form.addRow("Interfaz externa:", self.nat_external_iface_combo) + form.addRow("", enable_nat_btn) + form.addRow(QLabel("— Port Forwarding —")) + form.addRow("IP externa:", self.pf_ext_ip) + form.addRow("Puerto externo:", self.pf_ext_port) + form.addRow("IP interna:", self.pf_int_ip) + form.addRow("Puerto interno:", self.pf_int_port) + form.addRow("Protocolo:", self.pf_proto_combo) + form.addRow("", add_pf_btn) + + layout.addWidget(group) + + self.pf_table = QTableWidget() + self.pf_table.setColumnCount(4) + self.pf_table.setHorizontalHeaderLabels( + ["Externo", "Interno", "Proto", "Estado"] + ) + self.pf_table.horizontalHeader().setSectionResizeMode( + QHeaderView.ResizeMode.Stretch + ) + layout.addWidget(self.pf_table) + + self.tabs.addTab(tab, "NAT") + + def on_enable_nat_clicked(self): + internal = self.nat_internal_iface_combo.currentText() + external = self.nat_external_iface_combo.currentText() + if not (internal and external): + QMessageBox.warning( + self, + "Error", + "Selecciona interfaz interna y externa para habilitar NAT.", + ) + return + ok, msg = self.router.enable_nat_for_interface( + interface=internal, external_iface=external + ) + if ok: + QMessageBox.information(self, "NAT", msg) + else: + QMessageBox.critical(self, "Error NAT", msg) + + def on_add_pf_clicked(self): + ext_ip = self.pf_ext_ip.text().strip() + ext_port = self.pf_ext_port.value() + int_ip = self.pf_int_ip.text().strip() + int_port = self.pf_int_port.value() + proto = self.pf_proto_combo.currentText() + + if not (ext_ip and int_ip): + QMessageBox.warning( + self, + "Error", + "Debes indicar IP externa e interna para el port forwarding.", + ) + return + + ok, msg = self.router.add_port_forwarding( + external_ip=ext_ip, + external_port=ext_port, + internal_ip=int_ip, + internal_port=int_port, + protocol=proto, + ) + if ok: + QMessageBox.information(self, "Port forwarding", msg) + self.refresh_pf_table() + else: + QMessageBox.critical(self, "Error port forwarding", msg) + + def refresh_pf_table(self): + status = self.router.get_routing_status() + rules = status.get("nat_rules", []) + # Sólo mostramos las que tienen campos de port forwarding + pf_rules = [r for r in rules if "external_ip" in r] + self.pf_table.setRowCount(len(pf_rules)) + for row, r in enumerate(pf_rules): + self.pf_table.setItem( + row, + 0, + QTableWidgetItem( + f"{r.get('external_ip','')}:{r.get('external_port','')}" + ), + ) + self.pf_table.setItem( + row, + 1, + QTableWidgetItem( + f"{r.get('internal_ip','')}:{r.get('internal_port','')}" + ), + ) + self.pf_table.setItem(row, 2, QTableWidgetItem(r.get("protocol", ""))) + self.pf_table.setItem(row, 3, QTableWidgetItem("Activo")) + + # ------------------------------------------------------------------ + # Firewall / Visibilidad + # ------------------------------------------------------------------ + + def _init_firewall_tab(self): + tab = QWidget() + layout = QVBoxLayout(tab) + + group = QGroupBox("Visibilidad entre VLANs (simple)") + form = QFormLayout(group) + + self.fw_src_vlan = QSpinBox() + self.fw_src_vlan.setRange(1, 4094) + self.fw_dst_vlan = QSpinBox() + self.fw_dst_vlan.setRange(1, 4094) + self.fw_allow_cb = QCheckBox("Permitir tráfico (si no, se bloquea)") + self.fw_allow_cb.setChecked(True) + apply_btn = QPushButton("Aplicar política") + apply_btn.clicked.connect(self.on_apply_vlan_visibility_clicked) + + form.addRow("VLAN origen:", self.fw_src_vlan) + form.addRow("VLAN destino:", self.fw_dst_vlan) + form.addRow(self.fw_allow_cb) + form.addRow("", apply_btn) + + layout.addWidget(group) + + self.fw_table = QTableWidget() + self.fw_table.setColumnCount(3) + self.fw_table.setHorizontalHeaderLabels(["VLAN origen", "VLAN destino", "Permitido"]) + self.fw_table.horizontalHeader().setSectionResizeMode( + QHeaderView.ResizeMode.Stretch + ) + layout.addWidget(self.fw_table) + + self.tabs.addTab(tab, "Firewall") + + def on_apply_vlan_visibility_clicked(self): + src = self.fw_src_vlan.value() + dst = self.fw_dst_vlan.value() + allow = self.fw_allow_cb.isChecked() + + if allow: + ok, msg = self.router.allow_vlan_to_vlan(src, dst) + else: + ok, msg = self.router.block_vlan_to_vlan(src, dst) + + if ok: + QMessageBox.information(self, "Firewall", msg) + self.refresh_fw_table() + else: + QMessageBox.critical(self, "Error firewall", msg) + + def refresh_fw_table(self): + status = self.router.get_routing_status() + vis = status.get("vlan_visibility", {}) + items = list(vis.items()) + self.fw_table.setRowCount(len(items)) + for row, (k, allowed) in enumerate(items): + src_str, dst_str = k.split("-") + self.fw_table.setItem(row, 0, QTableWidgetItem(src_str)) + self.fw_table.setItem(row, 1, QTableWidgetItem(dst_str)) + self.fw_table.setItem(row, 2, QTableWidgetItem("Sí" if allowed else "No")) + + # ------------------------------------------------------------------ + # Utilidades comunes + # ------------------------------------------------------------------ + + def refresh_interfaces(self): + interfaces = self.nm.list_interfaces() + names = [i["name"] for i in interfaces] + + combos = [ + self.vlan_iface_combo, + self.src_iface_combo, + self.dst_iface_combo, + self.net_iface_combo, + self.route_iface_combo, + self.nat_internal_iface_combo, + self.nat_external_iface_combo, + ] + + for combo in combos: + combo.clear() + combo.addItems(names) + + # Tras refrescar interfaces, actualizamos también tablas en blanco + self.refresh_vlan_table() + self.refresh_redirection_table() + self.refresh_networks_table() + self.refresh_routes_table() + self.refresh_pf_table() + self.refresh_fw_table() + diff --git a/src/gui/widgets/interface_list.py b/src/gui/widgets/interface_list.py index 1e9b2ea..9bfb585 100644 --- a/src/gui/widgets/interface_list.py +++ b/src/gui/widgets/interface_list.py @@ -1,5 +1,5 @@ from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QListWidget, QListWidgetItem, - QLabel, QHBoxLayout, QFrame, QPushButton, QStyle) + QLabel, QHBoxLayout, QFrame, QPushButton, QStyle, QMessageBox) from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtGui import QIcon, QColor, QFont @@ -18,6 +18,14 @@ class InterfaceListWidget(QWidget): header_layout.addWidget(title) header_layout.addStretch() + # Reset Button + self.reset_btn = QPushButton("Reset") + self.reset_btn.setToolTip("Reiniciar interfaz seleccionada (down → up)") + self.reset_btn.setStyleSheet("background-color: #ff9800; color: white; padding: 4px 12px; font-weight: 600;") + self.reset_btn.clicked.connect(self.on_reset_clicked) + self.reset_btn.setEnabled(False) # Deshabilitado hasta que se seleccione una interfaz + header_layout.addWidget(self.reset_btn) + # Refresh Button self.refresh_btn = QPushButton() self.refresh_btn.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_BrowserReload)) @@ -28,6 +36,9 @@ class InterfaceListWidget(QWidget): self.layout.addLayout(header_layout) + # Guardar la interfaz seleccionada actual + self.current_selected_iface = None + # List self.list_widget = QListWidget() self.list_widget.itemClicked.connect(self.on_item_clicked) @@ -97,4 +108,33 @@ class InterfaceListWidget(QWidget): def on_item_clicked(self, item): iface_data = item.data(Qt.ItemDataRole.UserRole) + self.current_selected_iface = iface_data + self.reset_btn.setEnabled(True) # Habilitar botón reset cuando hay una interfaz seleccionada self.interface_selected.emit(iface_data) + + def on_reset_clicked(self): + """Reinicia la interfaz seleccionada.""" + if not self.current_selected_iface: + QMessageBox.warning(self, "Error", "No hay interfaz seleccionada.") + return + + iface_name = self.current_selected_iface['name'] + + # Confirmación + reply = QMessageBox.question( + self, + "Confirmar reinicio", + f"¿Estás seguro de que quieres reiniciar la interfaz '{iface_name}'?\n\n" + "Esto hará un down y luego up de la interfaz. La conexión se interrumpirá brevemente.", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No + ) + + if reply == QMessageBox.StandardButton.Yes: + success, msg = self.nm.reset_interface(iface_name) + if success: + QMessageBox.information(self, "Interfaz reiniciada", msg) + # Refrescar la lista para mostrar el nuevo estado + self.refresh_interfaces() + else: + QMessageBox.critical(self, "Error", msg) \ No newline at end of file diff --git a/src/gui/widgets/network_monitor.py b/src/gui/widgets/network_monitor.py new file mode 100644 index 0000000..973c5f2 --- /dev/null +++ b/src/gui/widgets/network_monitor.py @@ -0,0 +1,177 @@ +import subprocess +from typing import Dict + +import psutil +from PyQt6.QtCore import Qt, QTimer +from PyQt6.QtWidgets import ( + QWidget, + QVBoxLayout, + QHBoxLayout, + QLabel, + QTableWidget, + QTableWidgetItem, + QHeaderView, + QGroupBox, +) + +from src.core.network_manager import NetworkManager + + +class NetworkMonitorWidget(QWidget): + """ + Monitor de red en tiempo real: + - Tráfico por interfaz (bytes/seg, paquetes) + - Resumen básico de reglas de firewall (iptables FORWARD) + """ + + def __init__(self, network_manager: NetworkManager, parent=None): + super().__init__(parent) + self.nm = network_manager + + self.last_counters: Dict[str, psutil._common.snetio] = {} + + layout = QVBoxLayout(self) + + title = QLabel("Monitor de Red") + title.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) + title.setStyleSheet("font-size: 18px; font-weight: 600;") + layout.addWidget(title) + + # Grupo de tráfico por interfaz + traffic_group = QGroupBox("Tráfico por interfaz") + traffic_layout = QVBoxLayout(traffic_group) + + self.iface_table = QTableWidget() + self.iface_table.setColumnCount(7) + self.iface_table.setHorizontalHeaderLabels( + [ + "Interfaz", + "RX total (MB)", + "TX total (MB)", + "RX (KB/s)", + "TX (KB/s)", + "Paquetes RX/s", + "Paquetes TX/s", + ] + ) + self.iface_table.horizontalHeader().setSectionResizeMode( + QHeaderView.ResizeMode.Stretch + ) + traffic_layout.addWidget(self.iface_table) + + layout.addWidget(traffic_group) + + # Grupo de resumen de firewall + fw_group = QGroupBox("Resumen de firewall (iptables FORWARD)") + fw_layout = QVBoxLayout(fw_group) + + self.fw_table = QTableWidget() + self.fw_table.setColumnCount(5) + self.fw_table.setHorizontalHeaderLabels( + ["Regla", "Política", "Paquetes", "Bytes", "Destino"] + ) + self.fw_table.horizontalHeader().setSectionResizeMode( + QHeaderView.ResizeMode.Stretch + ) + fw_layout.addWidget(self.fw_table) + + layout.addWidget(fw_group) + + # Temporizador de actualización + self.timer = QTimer(self) + self.timer.setInterval(1000) # 1 segundo + self.timer.timeout.connect(self.refresh) + self.timer.start() + + # Inicial + self.refresh(initial=True) + + # ------------------------------------------------------------------ + # Actualización + # ------------------------------------------------------------------ + + def refresh(self, initial: bool = False): + self.refresh_traffic(initial=initial) + self.refresh_firewall() + + def refresh_traffic(self, initial: bool = False): + counters = psutil.net_io_counters(pernic=True) + + # Aseguramos que la tabla tenga una fila por interfaz conocida por NetworkManager + interfaces = [iface["name"] for iface in self.nm.list_interfaces()] + + self.iface_table.setRowCount(len(interfaces)) + + for row, name in enumerate(interfaces): + data = counters.get(name) + if not data: + continue + + last = self.last_counters.get(name) + if last and not initial: + seconds = 1.0 # timer fijo a 1s + rx_rate = (data.bytes_recv - last.bytes_recv) / 1024.0 / seconds + tx_rate = (data.bytes_sent - last.bytes_sent) / 1024.0 / seconds + rx_pkts = (data.packets_recv - last.packets_recv) / seconds + tx_pkts = (data.packets_sent - last.packets_sent) / seconds + else: + rx_rate = tx_rate = rx_pkts = tx_pkts = 0.0 + + self.iface_table.setItem(row, 0, QTableWidgetItem(name)) + self.iface_table.setItem( + row, 1, QTableWidgetItem(f"{data.bytes_recv / (1024.0 * 1024.0):.2f}") + ) + self.iface_table.setItem( + row, 2, QTableWidgetItem(f"{data.bytes_sent / (1024.0 * 1024.0):.2f}") + ) + self.iface_table.setItem(row, 3, QTableWidgetItem(f"{rx_rate:.1f}")) + self.iface_table.setItem(row, 4, QTableWidgetItem(f"{tx_rate:.1f}")) + self.iface_table.setItem(row, 5, QTableWidgetItem(f"{rx_pkts:.1f}")) + self.iface_table.setItem(row, 6, QTableWidgetItem(f"{tx_pkts:.1f}")) + + self.last_counters = counters + + def refresh_firewall(self): + """ + Lee iptables FORWARD para mostrar paquetes/bytes aceptados y bloqueados. + Si iptables no está disponible, deja la tabla vacía. + """ + try: + result = subprocess.run( + ["iptables", "-L", "FORWARD", "-v", "-n"], + capture_output=True, + text=True, + check=False, + ) + except FileNotFoundError: + self.fw_table.setRowCount(0) + return + + lines = result.stdout.splitlines() + rules = [] + # Saltamos las dos primeras líneas de cabecera + for line in lines[2:]: + parts = line.split() + if len(parts) < 8: + continue + pkts = parts[0] + bytes_ = parts[1] + target = parts[2] + # parts[3] prot, parts[4] opt, parts[5] in, parts[6] out, parts[7] source, parts[8] dest (si existe) + dest = parts[8] if len(parts) > 8 else "" + rules.append( + { + "target": target, + "pkts": pkts, + "bytes": bytes_, + "dest": dest, + } + ) + + self.fw_table.setRowCount(len(rules)) + for row, r in enumerate(rules): + self.fw_table.setItem(row, 0, QTableWidgetItem(f"Regla {row + 1}")) + self.fw_table.setItem(row, 1, QTableWidgetItem(r["target"])) + self.fw_table.setItem(row, 2, QTableWidgetItem(r["pkts"])) + self.fw_table.setItem(row, 3, QTableWidgetItem(r["bytes"])) + self.fw_table.setItem(row, 4, QTableWidgetItem(r["dest"])) diff --git a/src/gui/widgets/vlan35_check.py b/src/gui/widgets/vlan35_check.py new file mode 100644 index 0000000..4ad4429 --- /dev/null +++ b/src/gui/widgets/vlan35_check.py @@ -0,0 +1,522 @@ +import datetime +import os +from PyQt6.QtWidgets import ( + QWidget, + QVBoxLayout, + QHBoxLayout, + QLabel, + QPushButton, + QTableWidget, + QTableWidgetItem, + QHeaderView, + QGroupBox, + QFormLayout, + QLineEdit, + QSpinBox, + QComboBox, + QMessageBox, + QFileDialog, + QTextEdit, + QCheckBox, +) +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QColor + +from src.core.vlan35_checker import VLAN35Checker + + +class VLAN35CheckWidget(QWidget): + """ + Widget para comprobación de visibilidad en VLAN 35. + Ejecuta tests de conectividad TCP/UDP y genera informes HTML completos. + """ + + def __init__(self, parent=None): + super().__init__(parent) + self.checker = VLAN35Checker() + self.test_thread = None + self.test_results = [] + + layout = QVBoxLayout(self) + + title = QLabel("Comprobación VLAN35") + title.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) + title.setStyleSheet("font-size: 18px; font-weight: 600;") + layout.addWidget(title) + + # Grupo de configuración de tests + config_group = QGroupBox("Configuración de Tests") + config_layout = QVBoxLayout(config_group) + + # Botones de acción rápida + action_layout = QHBoxLayout() + load_defaults_btn = QPushButton("Cargar Tests por Defecto") + load_defaults_btn.clicked.connect(self.load_default_tests) + action_layout.addWidget(load_defaults_btn) + + self.run_btn = QPushButton("Ejecutar Tests") + self.run_btn.setStyleSheet("background-color: #008542; color: white; padding: 6px 16px;") + self.run_btn.clicked.connect(self.run_tests) + action_layout.addWidget(self.run_btn) + + self.stop_btn = QPushButton("Detener") + self.stop_btn.setEnabled(False) + self.stop_btn.setStyleSheet("background-color: #800000; color: white;") + self.stop_btn.clicked.connect(self.stop_tests) + action_layout.addWidget(self.stop_btn) + + action_layout.addStretch() + + generate_report_btn = QPushButton("Generar Informe HTML") + generate_report_btn.clicked.connect(self.generate_report) + action_layout.addWidget(generate_report_btn) + + config_layout.addLayout(action_layout) + + # Tabla de tests configurados + self.tests_table = QTableWidget() + self.tests_table.setColumnCount(5) + self.tests_table.setHorizontalHeaderLabels( + ["Host", "Puerto", "Protocolo", "Tipo", "Acción"] + ) + self.tests_table.horizontalHeader().setSectionResizeMode( + QHeaderView.ResizeMode.Stretch + ) + config_layout.addWidget(self.tests_table) + + # Formulario para añadir nuevo test + form_group = QGroupBox("Añadir Nuevo Test") + form_layout = QFormLayout(form_group) + + self.new_host_input = QLineEdit() + self.new_host_input.setPlaceholderText("10.47.8.133") + self.new_port_spin = QSpinBox() + self.new_port_spin.setRange(1, 65535) + self.new_port_spin.setValue(80) + self.new_proto_combo = QComboBox() + self.new_proto_combo.addItems(["TCP", "UDP"]) + self.new_type_combo = QComboBox() + self.new_type_combo.addItems(["connectivity", "ntp"]) + + add_test_btn = QPushButton("Añadir Test") + add_test_btn.clicked.connect(self.add_test) + + form_layout.addRow("Host:", self.new_host_input) + form_layout.addRow("Puerto:", self.new_port_spin) + form_layout.addRow("Protocolo:", self.new_proto_combo) + form_layout.addRow("Tipo:", self.new_type_combo) + form_layout.addRow("", add_test_btn) + + config_layout.addWidget(form_group) + layout.addWidget(config_group) + + # Grupo de resultados + results_group = QGroupBox("Resultados de Tests") + results_layout = QVBoxLayout(results_group) + + self.results_table = QTableWidget() + self.results_table.setColumnCount(7) + self.results_table.setHorizontalHeaderLabels( + [ + "Host", + "Puerto", + "Protocolo", + "Estado", + "Latencia (ms)", + "Detalles", + "Timestamp", + ] + ) + self.results_table.horizontalHeader().setSectionResizeMode( + QHeaderView.ResizeMode.Stretch + ) + results_layout.addWidget(self.results_table) + + # Log de progreso + self.log_output = QTextEdit() + self.log_output.setReadOnly(True) + self.log_output.setMaximumHeight(120) + self.log_output.setStyleSheet( + "background-color: #1e1e1e; color: #00ff00; font-family: monospace; font-size: 11px;" + ) + results_layout.addWidget(self.log_output) + + layout.addWidget(results_group) + + # Cargar tests por defecto al inicio + self.load_default_tests() + + def load_default_tests(self): + """Carga los tests por defecto del checker.""" + tests = self.checker.get_default_tests() + self.tests_table.setRowCount(len(tests)) + for row, test in enumerate(tests): + self.tests_table.setItem(row, 0, QTableWidgetItem(test["host"])) + self.tests_table.setItem(row, 1, QTableWidgetItem(str(test["port"]))) + self.tests_table.setItem(row, 2, QTableWidgetItem(test["protocol"])) + self.tests_table.setItem(row, 3, QTableWidgetItem(test.get("type", "connectivity"))) + + delete_btn = QPushButton("Eliminar") + delete_btn.setStyleSheet("background-color: #800000; color: white; padding: 2px 6px;") + delete_btn.clicked.connect( + lambda checked, r=row: self.remove_test(r) + ) + self.tests_table.setCellWidget(row, 4, delete_btn) + + def add_test(self): + """Añade un nuevo test a la tabla.""" + host = self.new_host_input.text().strip() + port = self.new_port_spin.value() + protocol = self.new_proto_combo.currentText() + test_type = self.new_type_combo.currentText() + + if not host: + QMessageBox.warning(self, "Error", "Debes indicar un host.") + return + + row = self.tests_table.rowCount() + self.tests_table.insertRow(row) + self.tests_table.setItem(row, 0, QTableWidgetItem(host)) + self.tests_table.setItem(row, 1, QTableWidgetItem(str(port))) + self.tests_table.setItem(row, 2, QTableWidgetItem(protocol)) + self.tests_table.setItem(row, 3, QTableWidgetItem(test_type)) + + delete_btn = QPushButton("Eliminar") + delete_btn.setStyleSheet("background-color: #800000; color: white; padding: 2px 6px;") + delete_btn.clicked.connect(lambda checked, r=row: self.remove_test(r)) + self.tests_table.setCellWidget(row, 4, delete_btn) + + # Limpiar formulario + self.new_host_input.clear() + self.new_port_spin.setValue(80) + + def remove_test(self, row: int): + """Elimina un test de la tabla.""" + self.tests_table.removeRow(row) + + def get_tests_from_table(self) -> list: + """Extrae la lista de tests desde la tabla.""" + tests = [] + for row in range(self.tests_table.rowCount()): + host = self.tests_table.item(row, 0) + port = self.tests_table.item(row, 1) + protocol = self.tests_table.item(row, 2) + test_type = self.tests_table.item(row, 3) + + if host and port and protocol: + tests.append( + { + "host": host.text(), + "port": int(port.text()), + "protocol": protocol.text(), + "type": test_type.text() if test_type else "connectivity", + } + ) + return tests + + def run_tests(self): + """Ejecuta todos los tests configurados.""" + tests = self.get_tests_from_table() + if not tests: + QMessageBox.warning(self, "Error", "No hay tests configurados.") + return + + self.test_results = [] + self.results_table.setRowCount(0) + self.log_output.clear() + self.log_output.append(f"Iniciando {len(tests)} tests...\n") + + self.run_btn.setEnabled(False) + self.stop_btn.setEnabled(True) + + self.test_thread = self.checker.run_tests(tests) + self.test_thread.test_result.connect(self.on_test_result) + self.test_thread.all_finished.connect(self.on_all_finished) + + def stop_tests(self): + """Detiene los tests en ejecución.""" + if self.test_thread and self.test_thread.isRunning(): + self.test_thread.terminate() + self.test_thread.wait() + self.run_btn.setEnabled(True) + self.stop_btn.setEnabled(False) + self.log_output.append("\nTests detenidos por el usuario.") + + def on_test_result(self, result: dict): + """Procesa el resultado de un test individual.""" + self.test_results.append(result) + + row = self.results_table.rowCount() + self.results_table.insertRow(row) + + # Host + self.results_table.setItem(row, 0, QTableWidgetItem(result["host"])) + # Puerto + self.results_table.setItem(row, 1, QTableWidgetItem(str(result["port"]))) + # Protocolo + self.results_table.setItem(row, 2, QTableWidgetItem(result["protocol"])) + # Estado + status_item = QTableWidgetItem(result["status"]) + if result["success"]: + status_item.setForeground(QColor("#4caf50")) + else: + status_item.setForeground(QColor("#f44336")) + self.results_table.setItem(row, 3, status_item) + # Latencia + latency_text = ( + f"{result['latency_ms']:.2f}" if result["latency_ms"] is not None else "N/A" + ) + self.results_table.setItem(row, 4, QTableWidgetItem(latency_text)) + # Detalles + details = result.get("details", result.get("error", "")) + self.results_table.setItem(row, 5, QTableWidgetItem(details)) + # Timestamp + self.results_table.setItem(row, 6, QTableWidgetItem(result["timestamp"])) + + # Log + status_icon = "✓" if result["success"] else "✗" + self.log_output.append( + f"{status_icon} {result['host']}:{result['port']} ({result['protocol']}) - {result['status']}" + ) + scrollbar = self.log_output.verticalScrollBar() + scrollbar.setValue(scrollbar.maximum()) + + def on_all_finished(self): + """Se ejecuta cuando todos los tests terminan.""" + self.run_btn.setEnabled(True) + self.stop_btn.setEnabled(False) + + success_count = sum(1 for r in self.test_results if r["success"]) + total_count = len(self.test_results) + + self.log_output.append( + f"\n{'='*50}\nTests completados: {success_count}/{total_count} exitosos" + ) + + def generate_report(self): + """Genera un informe HTML completo con todos los resultados.""" + if not self.test_results: + QMessageBox.warning( + self, "Error", "No hay resultados para generar el informe. Ejecuta los tests primero." + ) + return + + filename, _ = QFileDialog.getSaveFileName( + self, "Guardar Informe", "vlan35_check_report.html", "HTML Files (*.html)" + ) + + if not filename: + return + + html = self._generate_html_report() + try: + with open(filename, "w", encoding="utf-8") as f: + f.write(html) + QMessageBox.information( + self, "Informe Generado", f"Informe guardado en:\n{os.path.abspath(filename)}" + ) + except Exception as e: + QMessageBox.critical(self, "Error", f"Error al guardar informe:\n{str(e)}") + + def _generate_html_report(self) -> str: + """Genera el contenido HTML del informe.""" + timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + # Estadísticas + total = len(self.test_results) + success = sum(1 for r in self.test_results if r["success"]) + failed = total - success + + html = f""" + + + + Informe de Comprobación VLAN35 + + + +

+

Informe de Comprobación VLAN35

+ +
+

Fecha y hora: {timestamp}

+

Total de tests ejecutados: {total}

+
+
+
{success}
+
Exitosos
+
+
+
{failed}
+
Fallidos
+
+
+
{total}
+
Total
+
+
+
+ +

Resultados Detallados

+ + + + + + + + + + +""" + + for result in self.test_results: + status_class = "status-success" if result["success"] else "status-failed" + latency = ( + f"{result['latency_ms']:.2f}" if result["latency_ms"] is not None else "N/A" + ) + details = result.get("details", result.get("error", "")) + + html += f""" + + + + + + + + + +""" + + html += """ +
HostPuertoProtocoloEstadoLatencia (ms)DetallesTimestamp
{result['host']}{result['port']}{result['protocol']}{result['status']}{latency}{details}{result['timestamp']}
+ +

Resumen por Host

+ + + + + + + +""" + + # Agrupar por host + host_stats = {} + for result in self.test_results: + host = result["host"] + if host not in host_stats: + host_stats[host] = {"success": 0, "failed": 0} + if result["success"]: + host_stats[host]["success"] += 1 + else: + host_stats[host]["failed"] += 1 + + for host, stats in host_stats.items(): + total_host = stats["success"] + stats["failed"] + html += f""" + + + + + + +""" + + html += """ +
HostTests ExitososTests FallidosTotal
{host}{stats['success']}{stats['failed']}{total_host}
+
+ + +""" + + return html diff --git a/src/utils/__pycache__/report_generator.cpython-312.pyc b/src/utils/__pycache__/report_generator.cpython-312.pyc index 9f2b044f69bfe15b8bd623fc19719d29167e0146..d37bcf2686ec3dc388119f553346a452890aa511 100644 GIT binary patch delta 72 zcmeyS+oH#PnwOW00SF@dA}4a6F!0sS$j?pH&&p3MEX~Z*cgasK%}vcK$uHIqD9TSS aO3Y0x*7x#r*7Zv*3CYjTDc<;%Q49cOVHrjM delta 59 zcmZqC`=-l%nwOW00SL6+ + + + + Informe de Comprobación VLAN35 + + + +
+

Informe de Comprobación VLAN35

+ +
+

Fecha y hora: 2026-02-16 11:10:26

+

Total de tests ejecutados: 6

+
+
+
6
+
Exitosos
+
+
+
0
+
Fallidos
+
+
+
6
+
Total
+
+
+
+ +

Resultados Detallados

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HostPuertoProtocoloEstadoLatencia (ms)DetallesTimestamp
10.47.8.133515TCPOpen7.10Conexión TCP exitosa en 7.10ms2026-02-16 11:10:17
10.47.8.1339195TCPOpen7.88Conexión TCP exitosa en 7.88ms2026-02-16 11:10:17
10.47.8.1359191TCPOpen8.01Conexión TCP exitosa en 8.01ms2026-02-16 11:10:17
10.47.8.1359192TCPOpen4.41Conexión TCP exitosa en 4.41ms2026-02-16 11:10:17
10.240.225.171465TCPOpen3.96Conexión TCP exitosa en 3.96ms2026-02-16 11:10:17
10.244.255.250123UDPOpen5.11NTP hora local: 2026-02-16 11:25:012026-02-16 11:10:17
+ +

Resumen por Host

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HostTests ExitososTests FallidosTotal
10.47.8.133202
10.47.8.135202
10.240.225.171101
10.244.255.250101
+
+ +