From 43d57af624ce750995e5e17ab53a92b93a2c1d36 Mon Sep 17 00:00:00 2001 From: juakoknife <162052088+inlutec@users.noreply.github.com> Date: Thu, 20 Nov 2025 20:16:32 +0100 Subject: [PATCH] Add files via upload --- JNC-Scan_Launcher.sh | 28 + main.py | 28 + requirements.txt | 6 + src/__init__.py | 0 src/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 146 bytes .../__pycache__/diagnostics.cpython-312.pyc | Bin 0 -> 2703 bytes .../network_manager.cpython-312.pyc | Bin 0 -> 15855 bytes src/core/__pycache__/scanner.cpython-312.pyc | Bin 0 -> 6708 bytes src/core/__pycache__/sniffer.cpython-312.pyc | Bin 0 -> 5952 bytes .../__pycache__/vlan_scanner.cpython-312.pyc | Bin 0 -> 4691 bytes src/core/diagnostics.py | 58 ++ src/core/network_manager.py | 374 +++++++++++++ src/core/scanner.py | 128 +++++ src/core/sniffer.py | 138 +++++ src/core/vlan_scanner.py | 98 ++++ .../__pycache__/main_window.cpython-312.pyc | Bin 0 -> 8732 bytes src/gui/__pycache__/styles.cpython-312.pyc | Bin 0 -> 6537 bytes src/gui/main_window.py | 129 +++++ src/gui/styles.py | 165 ++++++ .../diagnostics_view.cpython-312.pyc | Bin 0 -> 11799 bytes .../interface_list.cpython-312.pyc | Bin 0 -> 6191 bytes .../__pycache__/ip_config.cpython-312.pyc | Bin 0 -> 9768 bytes .../__pycache__/port_scanner.cpython-312.pyc | Bin 0 -> 18893 bytes .../__pycache__/scanner_view.cpython-312.pyc | Bin 0 -> 10411 bytes .../__pycache__/sniffer_view.cpython-312.pyc | Bin 0 -> 10434 bytes .../__pycache__/vlan_bridge.cpython-312.pyc | Bin 0 -> 29391 bytes src/gui/widgets/diagnostics_view.py | 182 +++++++ src/gui/widgets/interface_list.py | 100 ++++ src/gui/widgets/ip_config.py | 165 ++++++ src/gui/widgets/port_scanner.py | 324 +++++++++++ src/gui/widgets/scanner_view.py | 163 ++++++ src/gui/widgets/sniffer_view.py | 162 ++++++ src/gui/widgets/vlan_bridge.py | 510 ++++++++++++++++++ .../report_generator.cpython-312.pyc | Bin 0 -> 5878 bytes src/utils/report_generator.py | 104 ++++ src/walkthrough.md.resolved | 86 +++ 36 files changed, 2948 insertions(+) create mode 100644 JNC-Scan_Launcher.sh create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 src/__init__.py create mode 100644 src/__pycache__/__init__.cpython-312.pyc create mode 100644 src/core/__pycache__/diagnostics.cpython-312.pyc create mode 100644 src/core/__pycache__/network_manager.cpython-312.pyc create mode 100644 src/core/__pycache__/scanner.cpython-312.pyc create mode 100644 src/core/__pycache__/sniffer.cpython-312.pyc create mode 100644 src/core/__pycache__/vlan_scanner.cpython-312.pyc create mode 100644 src/core/diagnostics.py create mode 100644 src/core/network_manager.py create mode 100644 src/core/scanner.py create mode 100644 src/core/sniffer.py create mode 100644 src/core/vlan_scanner.py create mode 100644 src/gui/__pycache__/main_window.cpython-312.pyc create mode 100644 src/gui/__pycache__/styles.cpython-312.pyc create mode 100644 src/gui/main_window.py create mode 100644 src/gui/styles.py create mode 100644 src/gui/widgets/__pycache__/diagnostics_view.cpython-312.pyc create mode 100644 src/gui/widgets/__pycache__/interface_list.cpython-312.pyc create mode 100644 src/gui/widgets/__pycache__/ip_config.cpython-312.pyc create mode 100644 src/gui/widgets/__pycache__/port_scanner.cpython-312.pyc create mode 100644 src/gui/widgets/__pycache__/scanner_view.cpython-312.pyc create mode 100644 src/gui/widgets/__pycache__/sniffer_view.cpython-312.pyc create mode 100644 src/gui/widgets/__pycache__/vlan_bridge.cpython-312.pyc create mode 100644 src/gui/widgets/diagnostics_view.py create mode 100644 src/gui/widgets/interface_list.py create mode 100644 src/gui/widgets/ip_config.py create mode 100644 src/gui/widgets/port_scanner.py create mode 100644 src/gui/widgets/scanner_view.py create mode 100644 src/gui/widgets/sniffer_view.py create mode 100644 src/gui/widgets/vlan_bridge.py create mode 100644 src/utils/__pycache__/report_generator.cpython-312.pyc create mode 100644 src/utils/report_generator.py create mode 100644 src/walkthrough.md.resolved diff --git a/JNC-Scan_Launcher.sh b/JNC-Scan_Launcher.sh new file mode 100644 index 0000000..ed8f97b --- /dev/null +++ b/JNC-Scan_Launcher.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +# Obtener el directorio donde está el script +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd "$DIR" + +# Comprobar si se está ejecutando en una terminal +if [ -t 0 ]; then + # Estamos en una terminal, pedir sudo directamente + echo "Iniciando JNC-Scan..." + sudo ./venv/bin/python3 main.py +else + # No estamos en una terminal (doble clic), intentar abrir una + if command -v gnome-terminal &> /dev/null; then + gnome-terminal -- bash -c "sudo $DIR/venv/bin/python3 main.py; exec bash" + elif command -v x-terminal-emulator &> /dev/null; then + x-terminal-emulator -e "sudo $DIR/venv/bin/python3 main.py" + elif command -v xterm &> /dev/null; then + xterm -e "sudo $DIR/venv/bin/python3 main.py" + else + # Fallback gráfico si no hay terminal detectada (usando pkexec si existe) + if command -v pkexec &> /dev/null; then + pkexec $DIR/venv/bin/python3 main.py + else + notify-send "Error" "No se encontró una terminal compatible para pedir contraseña de root." + fi + fi +fi diff --git a/main.py b/main.py new file mode 100644 index 0000000..9e4e604 --- /dev/null +++ b/main.py @@ -0,0 +1,28 @@ +import sys +import os +from PyQt6.QtWidgets import QApplication +from src.gui.main_window import MainWindow +from src.gui.styles import apply_styles + +def main(): + # Check for root privileges + if os.geteuid() != 0: + print("This application requires root privileges to manage network interfaces.") + print("Please run with sudo.") + # In a real GUI app we might want to show a dialog before exiting, + # but for now we'll just print to stdout. + # We can also attempt to restart with sudo, but that's complex for now. + sys.exit(1) + + app = QApplication(sys.argv) + + # Apply premium styles + apply_styles(app) + + window = MainWindow() + window.show() + + sys.exit(app.exec()) + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..edcf0f6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +PyQt6 +pyroute2 +scapy +python-nmap +netifaces +psutil diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/__pycache__/__init__.cpython-312.pyc b/src/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..83d69a7222a6584aa4ecfafd5b96a13afed9db11 GIT binary patch literal 146 zcmX@j%ge<81P=e@GC}lX5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!GStt=&rQ|O%1gjEsy$%s>_Z(H0@7 literal 0 HcmV?d00001 diff --git a/src/core/__pycache__/diagnostics.cpython-312.pyc b/src/core/__pycache__/diagnostics.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5704771b8496db641eaa49e4b20c4a833a29fd75 GIT binary patch literal 2703 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 literal 0 HcmV?d00001 diff --git a/src/core/__pycache__/scanner.cpython-312.pyc b/src/core/__pycache__/scanner.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ed9f817a4bbd82aecd06edcfa74f89769a35d1cd GIT binary patch literal 6708 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 literal 0 HcmV?d00001 diff --git a/src/core/__pycache__/sniffer.cpython-312.pyc b/src/core/__pycache__/sniffer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..71596267b0a77288043cd2a8f3f85525c5d29231 GIT binary patch literal 5952 zcmahNTW}NC^{#d$t!(*)A6mcgVr;MsvI_x92_+%efCF&=8wg@RQM7B2(JOb?fE77) zQ`$f#8B8A&Gbt0(W~O$@O!(*rGyO^0pJdt(Z%jvG!%RF=e)30wrVO1+xWY`7r<}0_lXX*RW9%; z@*c?eTDH%T6qh1_tTYHo*`Sx-OTjrIw|10xZT*8@JBVo{p;A)}1j4bf9tfDOa7O(z|R#lo=@UcnSJH8MmiVJlh%z;p!yp-52E0s#&8mt}eU(Z=qR z@u=E891p%e5{`BE#6u%dHKxb4?ic%a_y$73Shq$)-Jv*DyP2X?+Laik6|g>8S0Ih9 z9Az$;(^dj_I8JVhWfz==DBTnrGWD(Jg?Gy{4Q)_WWIT=Mh4cKV<;m72- z6Gl;$E^>hkKaRoB2#)3@m>q&Dr&_IE&z)*G%}e8c0O$A$pIh-3r#o}|ER7GNNL^8Z zQD#|%!wSz1mT*q|*nTZb2}efBN}}g=+eT>D7}Dl0T#LP7lyFheQH!jkxE$5Fnj=TR zF2}exxzTKWhC4&f@<+%S&gDFaKq3lE^abFX1M+tjxK{KuwCvf#bp|9D38-xs2+)1 zBf;ovN^o0pW#I{equaGL5)TC<+BRrdNcFh98~_}r>7Gya&g`A-*gQ^X9SosegqaEO z2^upUmK}ht=x|=mQm7IEYs~91E5aJsPS$1uBMD|1rmOdCNKNSBc#Pt)Gs_b+9#X+b z62Z_ZRktjsT%|Nl1MyI3gi=*81vMJhO&;vcYh(PGoXS{yRc0)jJg}0iTErbHRr|Tc z%UxTk%Qs)`Nms8ns#i}7pOnv(->m-O?Yfq!&gsEVj?WyQUAuL5>#+X zXAi8<&raaZK`(DQ655C!j+pM4st3YD;N*h3W(r~OhhBjR-*RzlU`NzE%;wyir3~5% zGs)&cWfYx-WfCYq@jO!YgH%tqphn@f)1Eq61)czc%P)mQr4q^@w$kf zm;6!=ZaDAOV8?99$$L2Oz;ZJLG?jA^cx^Wjg~z`XH;it&sO0uhgx}i7YvM%_SUH%BI-2!oG`e(S(Z%;5wGX#XGC00Im~pPY^}?9XH17=wMhu>gYwq zmBWB9=jRxeH95?MdQ8wgxn3$CAB@=-twj?lDr1gCSPexe&>eH89B{#uq?`atQ+9yv zD~>TS`Gm(of4tt!L<`ytX6NzQOKnAq9Va8lPBp z(6&Ht?5{drEvzsa|E$zMMq_@C(I{sTAFIY(z)2w%+I>+OD@nPOhEp975>+UTsS?mi z(-l4^smJ76>82g7VLc9RJ~kAm z&%nlX1A>hJynOf!xX3V%;N6uliY_rJ$i8iI(k35$#2rhP9Nl-IuYa)jz%kk64kShS z=z-pW-oaz0y%%qC(}DNIh-PvFCbvE5kdF===|5(c?1;x=YDkCh*|fd9hi<}8qUWaQ z*S|skos@c1O_vYG;6V~P$)cu?9dSk7B0u81Nyp%50-8yuY>iQL$Vu_=$+&znsKNMi z@}L@t$XWis&QW^KMyTVJHtLly46)f;d>r2L__L8j2~q&=6&n11?%QluL0^0&DFHssg)*oUOM(fc8bW( zERa3h_=Aa_c~8@mh|PY(sAfS+nX+ZT;k-OUE-ET{+>&t3J}Ny3_JHLtZ!0 zmnrPdd*rF@zkD;}>-{wR>CxGJ`)Ai201oPauLZv6ea~F)yZ-Er6AWj~{yYL>76fyr zs%Fx0sX5c;&1w?WU43;;x^=zLx_;pV6c4rgricDCFmo#1waw_-HtT!t#+gq~|2_2C z8|ht#ja`RlcfK;a@(3)iGb-gwJ8mu+m96s_jLU{wPP^L;cl*@If4I9J-l>oXC+V%f-HizE1K*r>k!h|HVcQV8KHSOuz~x5f^ebm@R;oRamxQC&kMdo?z_Z zg%7vvqeQ^>P;yzJnXxgz(J&qa72+-2$K>%i1HX9KFhZqQWIf`MLhU7kP zIRy+ZDFSrw+ByIa&yibo@>I=S-Kz1IZ`HO;@xKa;_y1cE>`nLFZoBVZlW3Rj)$Hf& zRo@QTID0c=gY6N5<@tg##ByH-u#hZ*G!XIgiX9R`oL^vxAp58QDXQ{*RX-D_r?QfJ zsy-mJe_S6F?|(06I&_x8qJz+7OlIFEERo_V+&`a5c@vSddZ}vwy_7PB3i3_@;NZn{ zKLG%;PWem;JiTnWj2qCjFur8oRUm*pz^Lx!AA6W#))Sm_wcZja+Af-(4M zsQq)~9#~`JeFqPqM6kD5c8NINGSj7nf{D?tU?jpaFjmNeHg82T7(%i5%XIG>-LG%z z+OO|`><&e{V19>g#}@Mz?9(T(i`gMY4rH4wpS6t+!x)|kc9_89a@^NsLz-;(nymeb bR6Gz)b6oSpt_K9`|0MV_uH_y9!1nq-4XF`! literal 0 HcmV?d00001 diff --git a/src/core/__pycache__/vlan_scanner.cpython-312.pyc b/src/core/__pycache__/vlan_scanner.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7c7311b1069a132732d406e4b566d4bd999060ec GIT binary patch literal 4691 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/wireless or use iw + is_wireless = False + try: + # Simple check if wireless extension exists + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: + fcntl.ioctl(s.fileno(), 0x8B01, struct.pack('256s', if_name.encode()[:15])) + is_wireless = True + except OSError: + pass + + interfaces.append({ + 'name': if_name, + 'mac': mac_address, + 'status': operstate, + 'ipv4': ipv4, + 'ipv6': ipv6, + 'type': 'Wireless' if is_wireless else 'Wired', + 'is_up': operstate == 'UP', + 'config': self.get_interface_config(if_name) # Add config details + }) + + return interfaces + + def get_interface_config(self, iface_name): + """ + Uses nmcli to get the current configuration of the interface. + Returns a dict with mode (auto/manual), ip, gateway, dns. + """ + config = { + 'method': 'unknown', + 'ip': '', + 'prefix': '', + 'gateway': '', + 'dns': '' + } + + try: + # 1. Get Connection Method (from connection profile) + cmd_conn = ["nmcli", "-t", "-f", "GENERAL.CONNECTION", "device", "show", iface_name] + res_conn = subprocess.run(cmd_conn, capture_output=True, text=True) + conn_name = res_conn.stdout.strip().split(':')[1] if ':' in res_conn.stdout else res_conn.stdout.strip() + + if conn_name: + cmd_method = ["nmcli", "-t", "-f", "ipv4.method", "connection", "show", conn_name] + res_method = subprocess.run(cmd_method, capture_output=True, text=True) + config['method'] = res_method.stdout.strip().split(':')[1] if ':' in res_method.stdout else res_method.stdout.strip() + + # 2. Get Dynamic/Applied Config (from device show) + # Fields: IP4.ADDRESS[1], IP4.GATEWAY, IP4.DNS[1] + cmd_dev = ["nmcli", "-t", "-f", "IP4.ADDRESS,IP4.GATEWAY,IP4.DNS", "device", "show", iface_name] + result = subprocess.run(cmd_dev, capture_output=True, text=True) + + lines = result.stdout.strip().split('\n') + for line in lines: + if ':' not in line: continue + key, value = line.split(':', 1) + value = value.strip() + + if key.startswith("IP4.ADDRESS"): + if '/' in value: + config['ip'], config['prefix'] = value.split('/') + else: + config['ip'] = value + elif key == "IP4.GATEWAY": + config['gateway'] = value + elif key.startswith("IP4.DNS"): + # Append multiple DNS if needed, for now just take the first or comma separate + if config['dns']: + config['dns'] += ", " + value + else: + config['dns'] = value + + except Exception: + pass + + return config + + def apply_interface_config(self, iface_name, method, ip=None, prefix=None, gateway=None, dns=None): + """ + Uses nmcli to apply configuration. + method: 'auto' (DHCP) or 'manual' (Static) + """ + try: + # Get connection name + cmd = ["nmcli", "-t", "-f", "GENERAL.CONNECTION", "device", "show", iface_name] + result = subprocess.run(cmd, capture_output=True, text=True) + conn_name = result.stdout.strip().split(':')[1] if ':' in result.stdout else result.stdout.strip() + + if not conn_name: + # Try to create a new connection if none exists? + # For now, assume one exists or fail. + return False, "No NetworkManager connection found for this interface." + + cmds = ["nmcli", "connection", "modify", conn_name, "ipv4.method", method] + + if method == 'manual': + if not ip or not prefix: + return False, "IP and Prefix required for manual mode." + cmds.extend(["ipv4.addresses", f"{ip}/{prefix}"]) + if gateway: + cmds.extend(["ipv4.gateway", gateway]) + if dns: + cmds.extend(["ipv4.dns", dns]) + else: + # Clear static settings when switching to auto + cmds.extend(["ipv4.addresses", "", "ipv4.gateway", "", "ipv4.dns", ""]) + + # Apply changes + subprocess.run(cmds, check=True) + + # Bring up the connection to apply + subprocess.run(["nmcli", "connection", "up", conn_name], check=True) + + return True, f"Configuration applied to {iface_name}" + + except subprocess.CalledProcessError as e: + return False, f"Failed to apply config: {str(e)}" + except Exception as e: + return False, str(e) + + def set_ip_config(self, iface_name, ip_address, prefix_len, gateway=None): + # Legacy method wrapper + return self.apply_interface_config(iface_name, 'manual', ip_address, prefix_len, gateway) + + def set_dhcp(self, iface_name): + # Legacy method wrapper + return self.apply_interface_config(iface_name, 'auto') + + def create_vlan_bridge(self, trunk_iface, vlan_id, access_iface): + """ + Creates a bridge between a VLAN on the trunk interface and an access interface. + 1. Create VLAN interface (e.g., eth0.35) + 2. Create Bridge (e.g., br35) + 3. Add VLAN interface and Access interface to Bridge + 4. Bring everything UP + """ + vlan_iface_name = f"{trunk_iface}.{vlan_id}" + bridge_name = f"br{vlan_id}" + + try: + # 1. Create VLAN interface + trunk_idx = self.ipr.link_lookup(ifname=trunk_iface)[0] + self.ipr.link('add', ifname=vlan_iface_name, kind='vlan', + link=trunk_idx, vlan_id=int(vlan_id)) + + # 2. Create Bridge + self.ipr.link('add', ifname=bridge_name, kind='bridge') + + # 3. Add ports to bridge + vlan_idx = self.ipr.link_lookup(ifname=vlan_iface_name)[0] + access_idx = self.ipr.link_lookup(ifname=access_iface)[0] + bridge_idx = self.ipr.link_lookup(ifname=bridge_name)[0] + + self.ipr.link('set', index=vlan_idx, master=bridge_idx) + self.ipr.link('set', index=access_idx, master=bridge_idx) + + # 4. Bring up interfaces + self.ipr.link('set', index=vlan_idx, state='up') + self.ipr.link('set', index=access_idx, state='up') + self.ipr.link('set', index=bridge_idx, state='up') + + return True, f"Bridge {bridge_name} created successfully." + + except Exception as e: + return False, str(e) + + def create_simple_bridge(self, iface1, iface2): + """ + Creates a simple bridge between two interfaces (Passthrough). + """ + bridge_name = "br_pass" + try: + # Check if bridge exists + if self.ipr.link_lookup(ifname=bridge_name): + return False, f"Bridge {bridge_name} already exists." + + # Create bridge + self.ipr.link('add', ifname=bridge_name, kind='bridge') + br_idx = self.ipr.link_lookup(ifname=bridge_name)[0] + + # Get indices + idx1 = self.ipr.link_lookup(ifname=iface1)[0] + idx2 = self.ipr.link_lookup(ifname=iface2)[0] + + # Helper to prepare interface + def prepare_interface(iface_name): + # Check if wireless + is_wireless = False + try: + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: + fcntl.ioctl(s.fileno(), 0x8B01, struct.pack('256s', iface_name.encode()[:15])) + is_wireless = True + except OSError: + pass + + if is_wireless: + # Try to enable 4addr mode (WDS) for bridging + try: + subprocess.run(["iw", "dev", iface_name, "set", "4addr", "on"], check=True, capture_output=True) + except subprocess.CalledProcessError: + # If 4addr fails, we proceed but warn/expect failure + pass + except FileNotFoundError: + # iw command not installed + pass + + prepare_interface(iface1) + prepare_interface(iface2) + + # Add ports to bridge + try: + self.ipr.link('set', index=idx1, master=br_idx) + self.ipr.link('set', index=idx2, master=br_idx) + except Exception as e: + if "95" in str(e) or "Operation not supported" in str(e): + # Cleanup + self.ipr.link('del', index=br_idx) + return False, (f"Bridging failed (Error 95). Wi-Fi interfaces in Client mode cannot be bridged " + f"unless they support '4addr' (WDS) mode. Your card/driver may not support this.") + raise e + + # Bring everything up + 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} created between {iface1} and {iface2}" + + except Exception as e: + # Cleanup if failed + try: + br_idx = self.ipr.link_lookup(ifname=bridge_name) + if br_idx: + self.ipr.link('del', index=br_idx[0]) + except: + pass + return False, str(e) + + def start_dhcp_server(self, interface, ip, range_start, range_end): + """ + Starts a dnsmasq DHCP server on the specified interface. + """ + try: + # 1. Configure IP on the interface + self.apply_interface_config(interface, 'manual', ip, '24') # Assuming /24 for simplicity + + # 2. Kill existing dnsmasq for this interface (simple cleanup) + subprocess.run(["pkill", "-f", f"dnsmasq.*{interface}"], check=False) + + # 3. Start dnsmasq + # --interface=br_pass --dhcp-range=192.168.x.x,192.168.x.y,12h --bind-interfaces + cmd = [ + "dnsmasq", + "--no-daemon", + f"--interface={interface}", + f"--dhcp-range={range_start},{range_end},12h", + f"--listen-address={ip}", + "--bind-interfaces" + ] + + # Run in background + subprocess.Popen(cmd) + + return True, f"DHCP Server started on {interface}" + + except Exception as e: + return False, str(e) + + def delete_bridge(self, bridge_name): + try: + # Cleanup DHCP if running + subprocess.run(["pkill", "-f", f"dnsmasq.*{bridge_name}"], check=False) + + idx = self.ipr.link_lookup(ifname=bridge_name) + if not idx: + return False, "Bridge not found" + + self.ipr.link('del', index=idx[0]) + return True, "Bridge deleted" + except Exception as e: + return False, str(e) + + def setup_wifi_ap(self, wifi_iface, ssid, password, bridge_name): + """ + Sets up a Wi-Fi Access Point bridged to the specified bridge. + Uses hostapd. + """ + import subprocess + import os + + # 1. Create hostapd config + hostapd_conf = f""" +interface={wifi_iface} +bridge={bridge_name} +driver=nl80211 +ssid={ssid} +hw_mode=g +channel=7 +wmm_enabled=0 +macaddr_acl=0 +auth_algs=1 +ignore_broadcast_ssid=0 +wpa=2 +wpa_passphrase={password} +wpa_key_mgmt=WPA-PSK +wpa_pairwise=TKIP +rsn_pairwise=CCMP +""" + conf_path = f"/tmp/hostapd_{wifi_iface}.conf" + with open(conf_path, "w") as f: + f.write(hostapd_conf) + + # 2. Kill existing hostapd on this interface + subprocess.run(["pkill", "-f", f"hostapd_{wifi_iface}.conf"], check=False) + + # 3. Start hostapd + try: + subprocess.Popen(["hostapd", conf_path]) + return True, "Wi-Fi AP started." + except Exception as e: + return False, str(e) + + def close(self): + self.ipr.close() diff --git a/src/core/scanner.py b/src/core/scanner.py new file mode 100644 index 0000000..a5abc16 --- /dev/null +++ b/src/core/scanner.py @@ -0,0 +1,128 @@ +import nmap +import subprocess +import shlex +import tempfile +import os +from PyQt6.QtCore import QThread, pyqtSignal + +import re + +class ScannerThread(QThread): + scan_finished = pyqtSignal(dict) + scan_error = pyqtSignal(str) + scan_progress = pyqtSignal(str) + port_found = pyqtSignal(int, str, str, str) # port, proto, state, service + + def __init__(self, target, arguments): + super().__init__() + self.target = target + self.arguments = arguments + self.process = None + self.temp_xml = None + + def run(self): + try: + # Create temp file for XML output + fd, self.temp_xml = tempfile.mkstemp(suffix='.xml') + os.close(fd) + + # Construct command: nmap -oX -v + # -v is important to get progress output on stdout + cmd = ["nmap", "-oX", self.temp_xml, "-v"] + shlex.split(self.arguments) + [self.target] + + self.scan_progress.emit(f"Executing: {' '.join(cmd)}\n") + + self.process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1) + + # Regex for discovered ports: "Discovered open port 80/tcp on 192.168.1.1" + port_regex = re.compile(r"Discovered open port (\d+)/(\w+) on") + + # Read stdout line by line for progress + while True: + line = self.process.stdout.readline() + if not line and self.process.poll() is not None: + break + if line: + self.scan_progress.emit(line.strip()) + # Check for discovered port + match = port_regex.search(line) + if match: + port = int(match.group(1)) + proto = match.group(2).upper() + self.port_found.emit(port, proto, "open", "") + + stdout, stderr = self.process.communicate() + + if self.process.returncode == 0: + # Parse XML output from file + nm = nmap.PortScanner() + try: + with open(self.temp_xml, 'r') as f: + xml_content = f.read() + + if not xml_content: + self.scan_error.emit("Scan produced no output.") + return + + result = nm.analyse_nmap_xml_scan(xml_content) + if result is None: + result = nm.scan_result + self.scan_finished.emit(result) + except Exception as e: + self.scan_error.emit(f"Error parsing scan results: {str(e)}") + else: + # If killed or failed + if self.process.returncode == -9: # SIGKILL + self.scan_progress.emit("\nScan stopped by user.") + self.scan_error.emit("Scan stopped by user.") + else: + self.scan_error.emit(f"Scan failed: {stderr}") + + except Exception as e: + self.scan_error.emit(str(e)) + finally: + # Cleanup temp file + if self.temp_xml and os.path.exists(self.temp_xml): + try: + os.remove(self.temp_xml) + except: + pass + + def stop(self): + if self.process: + self.process.kill() + self.wait() + +class NetworkScanner: + def __init__(self): + pass + + def scan(self, target, ports=None, scan_type="TCP", skip_discovery=False): + """ + Initiates a scan. Returns the thread object. + target: IP or range (e.g., '192.168.1.1' or '192.168.1.0/24') + ports: String of ports (e.g., '80,443' or '1-1000') + scan_type: 'TCP' or 'UDP' + skip_discovery: If True, adds -Pn (treat all as online). If False, performs host discovery (ARP/Ping). + """ + # -T4: Aggressive timing + # --stats-every 5s: Print progress line every 5 seconds + # --min-rate 1000: Send packets at least 1000 per second (Speed up) + arguments = "-T4 --stats-every 5s --min-rate 1000" + + if skip_discovery: + arguments += " -Pn" + + if scan_type == "UDP": + arguments += " -sU" + else: + arguments += " -sT" # Connect scan (safer for non-root) + + if ports: + arguments += f" -p {ports}" + else: + arguments += " -F" # Fast scan if no ports specified + + thread = ScannerThread(target, arguments) + thread.start() + return thread diff --git a/src/core/sniffer.py b/src/core/sniffer.py new file mode 100644 index 0000000..5d33643 --- /dev/null +++ b/src/core/sniffer.py @@ -0,0 +1,138 @@ +from scapy.all import sniff, IP, TCP, UDP, ARP, ICMP +from PyQt6.QtCore import QThread, pyqtSignal +import datetime + +class SnifferThread(QThread): + packet_received = pyqtSignal(dict) + error_occurred = pyqtSignal(str) + + def __init__(self, interface, filter_str=""): + super().__init__() + self.interface = interface + self.filter_str = filter_str + self.running = True + + def run(self): + try: + # Check if interface is up + if not self.is_interface_up(self.interface): + raise Exception(f"Interface {self.interface} is DOWN. Please enable it first.") + + sniff(iface=self.interface, + prn=self.process_packet, + filter=self.filter_str, + store=0, + stop_filter=lambda x: not self.running) + except Exception as e: + self.error_occurred.emit(str(e)) + + def is_interface_up(self, interface): + try: + import psutil + stats = psutil.net_if_stats() + if interface in stats: + return stats[interface].isup + return False + except: + return True # Assume up if check fails to avoid blocking + + def process_packet(self, packet): + if not self.running: + return + + try: + pkt_info = { + 'time': datetime.datetime.now().strftime("%H:%M:%S.%f")[:-3], + 'src': 'Unknown', + 'dst': 'Unknown', + 'proto': 'Unknown', + 'info': packet.summary() + } + + if IP in packet: + pkt_info['src'] = packet[IP].src + pkt_info['dst'] = packet[IP].dst + pkt_info['proto'] = packet[IP].proto + + # Printer Ports + printer_ports = [631, 515, 9100] + + if TCP in packet: + pkt_info['proto'] = 'TCP' + flags = packet[TCP].flags + flag_str = str(flags) + + sport = packet[TCP].sport + dport = packet[TCP].dport + + info = f"{sport} -> {dport} [{flag_str}]" + + # Check for Printer Traffic + if sport in printer_ports or dport in printer_ports: + info = f"[PRINTER] {info}" + + # Check for potential issues (simple heuristics) + if 'R' in flag_str: # RST + info += " [RESET]" + pkt_info['status'] = 'Error' + elif 'S' in flag_str and 'A' not in flag_str: # SYN only + info += " [SYN]" + pkt_info['status'] = 'Connecting' + else: + pkt_info['status'] = 'OK' + + pkt_info['info'] = info + + elif UDP in packet: + pkt_info['proto'] = 'UDP' + sport = packet[UDP].sport + dport = packet[UDP].dport + info = f"{sport} -> {dport}" + + if sport in printer_ports or dport in printer_ports: + info = f"[PRINTER] {info}" + + pkt_info['info'] = info + pkt_info['status'] = 'OK' + + elif ICMP in packet: + pkt_info['proto'] = 'ICMP' + icmp_type = packet[ICMP].type + icmp_code = packet[ICMP].code + + if icmp_type == 3: # Destination Unreachable + pkt_info['info'] = f"Dest Unreachable (Code: {icmp_code})" + pkt_info['status'] = 'Error' + else: + pkt_info['info'] = f"Type: {icmp_type} Code: {icmp_code}" + pkt_info['status'] = 'OK' + + elif ARP in packet: + pkt_info['proto'] = 'ARP' + pkt_info['src'] = packet[ARP].psrc + pkt_info['dst'] = packet[ARP].pdst + pkt_info['info'] = f"Who has {packet[ARP].pdst}? Tell {packet[ARP].psrc}" + pkt_info['status'] = 'OK' + + self.packet_received.emit(pkt_info) + + except Exception: + pass + + def stop(self): + self.running = False + +class NetworkSniffer: + def __init__(self): + self.thread = None + + def start_sniffing(self, interface, filter_str=""): + self.thread = SnifferThread(interface, filter_str) + self.thread.start() + return self.thread + + def stop_sniffing(self): + if self.thread: + self.thread.stop() + self.thread.wait() + self.thread = None diff --git a/src/core/vlan_scanner.py b/src/core/vlan_scanner.py new file mode 100644 index 0000000..380dce4 --- /dev/null +++ b/src/core/vlan_scanner.py @@ -0,0 +1,98 @@ +from PyQt6.QtCore import QThread, pyqtSignal +import subprocess +import re +import time + +class VLANScannerThread(QThread): + vlan_detected = pyqtSignal(int, int) # vlan_id, count + untagged_detected = pyqtSignal(int) # count + scan_finished = pyqtSignal() + error_occurred = pyqtSignal(str) + + def __init__(self, interface, timeout=10): + super().__init__() + self.interface = interface + self.timeout = timeout + self.running = True + self.process = None + self.detected_vlans = {} # vlan_id -> count + self.untagged_count = 0 + + def run(self): + try: + # Use tcpdump to capture ALL packets to detect untagged vs tagged + # -i: interface + # -e: print link-level header (to see vlan) + # -n: don't resolve names + # -l: buffered output + # not arp and not stp: filter out some noise if desired, but let's keep it simple + cmd = ["tcpdump", "-i", self.interface, "-e", "-n", "-l"] + + self.process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True, + bufsize=1 + ) + + start_time = time.time() + + while self.running: + # Check timeout + if time.time() - start_time > self.timeout: + break + + # Non-blocking read line + line = self.process.stdout.readline() + if not line and self.process.poll() is not None: + break + + if line: + self.parse_line(line) + + except Exception as e: + self.error_occurred.emit(str(e)) + finally: + if self.process: + self.process.terminate() + try: + self.process.wait(timeout=1) + except: + self.process.kill() + self.scan_finished.emit() + + def parse_line(self, line): + # Example Tagged: ... ethertype 802.1Q (0x8100), length 100: vlan 35, p 0, ethertype IPv4 ... + # Example Untagged: ... ethertype IPv4 (0x0800), length 100: ... + + # Regex to find "vlan " + match = re.search(r"vlan (\d+)", line) + if match: + vlan_id = int(match.group(1)) + if vlan_id not in self.detected_vlans: + self.detected_vlans[vlan_id] = 0 + + self.detected_vlans[vlan_id] += 1 + self.vlan_detected.emit(vlan_id, self.detected_vlans[vlan_id]) + else: + # It's an untagged packet (or at least not 802.1Q) + # Filter out empty lines or weird output + if "ethertype" in line: + self.untagged_count += 1 + self.untagged_detected.emit(self.untagged_count) + + def stop(self): + self.running = False + if self.process: + self.process.kill() # Force kill to break readline() + self.wait() + +class VLANScanner: + def __init__(self): + pass + + def scan(self, interface, timeout=10): + thread = VLANScannerThread(interface, timeout) + thread.start() + return thread diff --git a/src/gui/__pycache__/main_window.cpython-312.pyc b/src/gui/__pycache__/main_window.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..941de8759856d32ceb3b657061e433f46149e163 GIT binary patch literal 8732 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 literal 0 HcmV?d00001 diff --git a/src/gui/main_window.py b/src/gui/main_window.py new file mode 100644 index 0000000..01576c8 --- /dev/null +++ b/src/gui/main_window.py @@ -0,0 +1,129 @@ +from PyQt6.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QTabWidget, + QLabel, QHBoxLayout, QStatusBar) +from PyQt6.QtCore import Qt +from src.core.network_manager import NetworkManager +from src.gui.widgets.interface_list import InterfaceListWidget +from src.gui.widgets.ip_config import IPConfigWidget +from src.gui.widgets.vlan_bridge import VLANBridgeWidget +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.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.nm = NetworkManager() + + # Central Widget + central_widget = QWidget() + self.setCentralWidget(central_widget) + main_layout = QHBoxLayout(central_widget) + + # Sidebar (Interface List) + 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) + self.tabs = QTabWidget() + main_layout.addWidget(self.tabs) + + # IP Configuration Tab + self.ip_config_widget = IPConfigWidget(self.nm) + self.tabs.addTab(self.ip_config_widget, "IP Configuration") + + # VLAN & Bridge Tab + self.vlan_bridge_widget = VLANBridgeWidget(self.nm) + self.tabs.addTab(self.vlan_bridge_widget, "VLAN / Bridge") + + # Scanner Tab (Network Discovery) + self.scanner_widget = ScannerWidget() + self.tabs.addTab(self.scanner_widget, "Net Scanner") + + # Port Scanner Tab (Dedicated) + self.port_scanner_widget = PortScannerWidget() + self.tabs.addTab(self.port_scanner_widget, "Port Scanner") + + # Sniffer Tab + self.sniffer_widget = SnifferWidget(self.nm) + self.tabs.addTab(self.sniffer_widget, "Sniffer") + + # Diagnostics Tab + self.diagnostics_widget = DiagnosticsWidget() + self.tabs.addTab(self.diagnostics_widget, "Diagnostics") + + # 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) + + # Status Bar + self.status_bar = QStatusBar() + self.setStatusBar(self.status_bar) + self.status_bar.showMessage("Ready") + + def on_interface_selected(self, iface_data): + self.status_bar.showMessage(f"Selected Interface: {iface_data['name']}") + self.ip_config_widget.set_interface(iface_data) + + def closeEvent(self, event): + self.nm.close() + self.scanner_widget.close() + self.port_scanner_widget.close() + self.vlan_bridge_widget.close() + event.accept() + + def generate_report(self): + # Gather data + interfaces = self.nm.list_interfaces() + + # Get scan results from scanner widget + scan_results = [] + for row in range(self.scanner_widget.table.rowCount()): + scan_results.append({ + 'ip': self.scanner_widget.table.item(row, 0).text(), + 'mac': self.scanner_widget.table.item(row, 1).text(), + 'vendor': self.scanner_widget.table.item(row, 2).text(), + 'status': self.scanner_widget.table.item(row, 3).text() + }) + + # Get sniffer results (All Traffic) + sniff_all = [] + for row in range(self.sniffer_widget.table_all.rowCount()): + sniff_all.append({ + 'time': self.sniffer_widget.table_all.item(row, 0).text(), + 'src': self.sniffer_widget.table_all.item(row, 1).text(), + 'dst': self.sniffer_widget.table_all.item(row, 2).text(), + 'proto': self.sniffer_widget.table_all.item(row, 3).text(), + 'info': self.sniffer_widget.table_all.item(row, 4).text(), + 'status': self.sniffer_widget.table_all.item(row, 5).text() + }) + + # Get sniffer results (Failed/Anomalies) + sniff_failed = [] + for row in range(self.sniffer_widget.table_failed.rowCount()): + sniff_failed.append({ + 'time': self.sniffer_widget.table_failed.item(row, 0).text(), + 'src': self.sniffer_widget.table_failed.item(row, 1).text(), + 'dst': self.sniffer_widget.table_failed.item(row, 2).text(), + 'proto': self.sniffer_widget.table_failed.item(row, 3).text(), + 'info': self.sniffer_widget.table_failed.item(row, 4).text(), + 'status': self.sniffer_widget.table_failed.item(row, 5).text() + }) + + generator = ReportGenerator() + filename, _ = QFileDialog.getSaveFileName(self, "Save Report", "report.html", "HTML Files (*.html)") + + if filename: + success, msg = generator.generate_html_report(interfaces, scan_results, sniff_all, sniff_failed, filename) + if success: + self.status_bar.showMessage(f"Report saved to {msg}") + else: + self.status_bar.showMessage(f"Error saving report: {msg}") diff --git a/src/gui/styles.py b/src/gui/styles.py new file mode 100644 index 0000000..e44c519 --- /dev/null +++ b/src/gui/styles.py @@ -0,0 +1,165 @@ +from PyQt6.QtWidgets import QApplication +from PyQt6.QtGui import QPalette, QColor + +def apply_styles(app: QApplication): + """Applies a premium dark theme to the application.""" + 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) + + palette.setColor(QPalette.ColorRole.Window, dark_bg) + palette.setColor(QPalette.ColorRole.WindowText, text_color) + palette.setColor(QPalette.ColorRole.Base, darker_bg) + palette.setColor(QPalette.ColorRole.AlternateBase, dark_bg) + palette.setColor(QPalette.ColorRole.ToolTipBase, text_color) + palette.setColor(QPalette.ColorRole.ToolTipText, text_color) + palette.setColor(QPalette.ColorRole.Text, text_color) + palette.setColor(QPalette.ColorRole.Button, dark_bg) + palette.setColor(QPalette.ColorRole.ButtonText, text_color) + palette.setColor(QPalette.ColorRole.BrightText, Qt.GlobalColor.red) + 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 + app.setStyleSheet(""" + QMainWindow { + background-color: #181818; + } + QWidget { + font-family: 'Segoe UI', 'Roboto', sans-serif; + font-size: 13px; + color: #e0e0e0; + } + QTabWidget::pane { + border: 1px solid #333333; + background: #252526; + border-radius: 4px; + top: -1px; + } + QTabBar::tab { + background: #1e1e1e; + color: #999999; + padding: 10px 20px; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + margin-right: 2px; + font-weight: 600; + border: 1px solid transparent; + } + QTabBar::tab:selected { + background: #252526; + color: #ffffff; + border-top: 2px solid #007acc; + border-bottom: 1px solid #252526; + } + QTabBar::tab:hover { + background: #2d2d2d; + color: #ffffff; + } + QPushButton { + background-color: #007acc; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: 600; + } + QPushButton:hover { + background-color: #0062a3; + } + QPushButton:pressed { + background-color: #004c80; + } + QPushButton:disabled { + background-color: #333333; + color: #666666; + } + QLineEdit, QComboBox, QSpinBox { + background-color: #333333; + border: 1px solid #444444; + color: #f0f0f0; + padding: 6px; + border-radius: 3px; + } + QLineEdit:focus, QComboBox:focus, QSpinBox:focus { + border: 1px solid #007acc; + background-color: #3c3c3c; + } + QTableWidget { + background-color: #1e1e1e; + border: 1px solid #333333; + border-radius: 4px; + gridline-color: #333333; + selection-background-color: #264f78; + selection-color: white; + alternate-background-color: #252526; + } + QHeaderView::section { + background-color: #252526; + color: #cccccc; + padding: 6px; + border: none; + border-right: 1px solid #333333; + border-bottom: 1px solid #333333; + font-weight: 600; + } + QListWidget { + background-color: #1e1e1e; + border: 1px solid #333333; + border-radius: 4px; + } + QListWidget::item { + padding: 8px; + border-bottom: 1px solid #252526; + } + QListWidget::item:selected { + background-color: #37373d; + color: white; + border-left: 2px solid #007acc; + } + QGroupBox { + border: 1px solid #444444; + 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; + left: 10px; + } + QStatusBar { + background-color: #007acc; + color: white; + } + QProgressBar { + border: 1px solid #444444; + border-radius: 4px; + text-align: center; + background-color: #333333; + } + QProgressBar::chunk { + background-color: #007acc; + width: 10px; + } + QSplitter::handle { + background-color: #333333; + } + """) + +from PyQt6.QtCore import Qt diff --git a/src/gui/widgets/__pycache__/diagnostics_view.cpython-312.pyc b/src/gui/widgets/__pycache__/diagnostics_view.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f494d625a9209a91d9c5f9c1eb4ac175f0a71dda GIT binary patch literal 11799 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 literal 0 HcmV?d00001 diff --git a/src/gui/widgets/__pycache__/ip_config.cpython-312.pyc b/src/gui/widgets/__pycache__/ip_config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..da359fb67a15040d42699b7f1dc1673b71cda5be GIT binary patch literal 9768 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 F{{qEo8BzcM literal 0 HcmV?d00001 diff --git a/src/gui/widgets/__pycache__/scanner_view.cpython-312.pyc b/src/gui/widgets/__pycache__/scanner_view.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3bf19de86989bad49cd99a65e59a601cec11838f GIT binary patch literal 10411 zcmdT~U2GdycAnv%hJPYuTe5y_TK}{qN{)YGOLlf`$v<&oMY0qx!p31~&PXCna+Eux zI1;GcU=g4a>P2iUwvplPnrQ<>Y4$;nedt3U3v3H`OuEAk^5CLI0l#_eYu7HD#oiD$Dwe9G_)&!(9S z8)Y-Ls4ZiU+B1%*Bjb!ZGp?vBQx~nva8WMfj=D3Rs3+r%dNaPLFXNB;GxgDW7WHlE zKqeRsvW$&6$ME)#8QvkVU)hazq76&VuV4RKFd%xV5|GseggY(;ChW;mbE?ztG|SL~;ZY*wy0a?CHF{;M*F zWn{X-CNIat%B#V^dO^uS)nzZGa?Drd7NH(mF_Ijh6t%y?$_>>#&~Q^;j!EsHw3VkC zEt*n1OIw6K?>4zM$_1-F$_1;al1_&B5;fyNXe5quER4t$dqa_gfN_vU;6kQwJ#5%H=^y^A0LYbC#*C1O2SA zX5Xh((UOzO()>9s)tJi{(5kWe65k%B$$&| zC~f7bBNk05-o?AVDjx?h?%H~HCY% zY>mEJmgG06b>2<6E4GUB(>%9U?D9n_OUDJP&_{VNpA(d}^3;??Q;PS{mh?SROKY0S zS~>6iUahw+X-e_F3N3rBlcjdQE3|6=3J=g0tUPDV70WC!VgJN(_e`7sEC`hyZm3^~ z%tb;6kG~Q*c=TB0U}SjcmBZn0edr;nOxUj3Po>kE?d=84F)m)uF4q}GMwaW&CeyM& zMhN~YAEP@AJ2l(ulbZeFsWHuUS;+Dt(OlDVTwauxJ#Q@t+0dj&WNBIV!66D0W*+|y z=q{hW8c$rC16Yvd2NPmiBqO1&g984IhG=7t?@u5W)}+#W>y6RXMBGCDO^nHGv001O$_iHID=~f+3BI^E{#`N!yWFBt*!{Xm)Hqo}B|L{`kT|dMRdTNSYgj4X{EGFz96i(GkDi zkA*Vaf*uL-CA6VA=9AD@L<^czMjWTrL*1B|Ud&|2V1{L_1%B9$8hF&+bLS1LZYgm- zm1|MB7L^-NxPg0v(?xFPQFB**qR@N@bo?OjiGR(navciSp>jP6*OTYJ5I+-(+|in5 zIu)+-e%G-gcf88B${kj?!<#eek#owCb4Bj_LvFXq?NhjYpZ9LG6gm$Txx*#O%6F@s zhm@_n7Zl5Pe=+#kV38Ys2wnHCd5x~GLx&?U0WCdu>bHz*kHYO)e?Pyk$aR}zZB@9| zBG-;(%K(2jb30Sy2Ea&D=P%y_BOVm+ta*%4m?m@%&gT8WcZ=K=Q>aVfx(Zy6%8e-8 z$Ulv$qf^T0lsfv3GWt%Di&i(5x}7R=VQ8#z&$?LH66Gfa)TxZ*X~{~SiM%_cBovt!nLo@+>PCim4&RKCLeJ9 z&tQi!TP`Vh2I!ZRe}(U-2!v3iDy7Veh*d$IB+p(0z)P-xrg=fJq{w7#ehT+`RiYMk zw^>z(#gutnu`2lrr?gGq@m#4&yxnquOE~6QaN{{+USV^r)NEa|Qg}jDr5s$Sc_+P4 zqsx4W{#5|pZM1e)H3vliwB^g7K7+fyE0HoUY*yWtS+4=uQ*D+krK*5tJLOw>YOh68 zYD-HY%C+)Tmqk-52e)&c<88lne^uUH0Lsd+oxvHcvFB3VRNKl^CXlmup#A)-T5aq5 zEen*|#`*xQxAHA3RIBe1OO{gGSRbbKR&H4bfaaN4c2CRVLI|DB*<~-%3(4$UBofh_ zX9y9=^4`g`5SN4y_%MNl;voa6gkGNv5%l|!<^CEV>ew(Q6GUB~Ps+lm4%#&P%-AF# zJm&(!GR=*?FD5T7AY8bXT!`^WDItC!kfm@7!RxfZAnS1E3?wXR?Se0Q~^1 z!y{Zyg1=C$?s}YLVF&~-KLjTLy!yO&og6|^2L$4E9gq;b7HG}{jE@iu6o4S~22cU^ zX%@hoG%xT3*Ix^YSshxLZ0H6`2{=cZpVpR(dXULJ(m6we!(9xC2!!9lWQkcZ-?UTD0uhgNAG(hrNB-#u=jpoZz;G- z4Th9pNDcNV!JcBU4>+ItZ~4_=zY^@R&|&)plEEc&KU7`%{)5!SC2x&7#KbPoEZd)*U7?k$s5 z;aqi3X*l2_x6gD`2sMh_-X|Q>9=dzt_6fCZKxrG;xcrZY|K-TN`0Ty5fkNAyj&49j z<(dKes9dkY^{QM%;UXKdIy9~ffj2x;QI9=QxdRG!U}Ibzcug62t;qeTqW-)354baA z9A-LU9wJb{A0i(2&wdM^mvMrXk%?^%YDAA!i5`S2ww$e&BLr>@S4Xuff8<}X6Je*Z z*IfYf;fka+zz2C4a*wv9*}$k0|~~!9ToeD{&q~T{lmE zdg0cEU%vT(+xOj7z-y4c3Y)MVFS81jG07KSg%^MhH9$%wbbz+7&s8bqtztltt{pgL zHig%$s>C~z42?#?u3%Dl%d1K$yn0q8*@OYs?cCZQvKhaH9rYa7Pc_Dwo9vZUhIto$ z8!LAG>sWE}&J_rOuhiw7ybA+ud>wvQIG)QnKVV2F?}i!T)an@3glc+ z<0(slra|X)iz{_=%!-@$>XOHx&Wm!->+v{q1-ypdL(hsg=QaAo`w)ss?2SOqok9~; zDd)Mv@cv&{K*k&04u-Ec+pYgUY&THdZq7{__+Tw~l`~^iS(dl2__o;(W`xihX3owx z$R_m4S+QjNT|v@;VXeJY+t!W>TPC=)Ani-_YiETmGfw#fZSAVC#nIjS18p@{*fOJO zmaUuYjlXsv&_obXm8@Siz^$f<8G^qzjUm&R)xDj)mW2S9<^mTF5i1BKK`!X=7JSVT zB+Z_QCp6awMqtY>Cm9V;e~g~%F)rw1^jqNu9ZM06 zUXls?nBa(DEL^igh)xGc1cQ~DJuPH4w~&R%1;PF}0lsA~j0^HT^nJnHDpFMaW|&wf_uJ67m8{)}mJ zpJvxCuAW+3zG=HTUfR*NI#z1jvwF7F*s|V#>%IIrrEy^O?889oIcnqe@fXhEr*Z+_aZ=wyn#`&aMsnhP-+H%bzO!Crcgs^8L5p+Za*sE*Y`6-$Bckpj(O$8y5p|&8vl;<3-;{sa9R0&@)o>jn=H2E%cl$`d)kKx@4i}$N%m-_2gAt zq9-Ga8zNq~)(^OL1AXAni6+EpI{G*YTk2)d2R#K+&qI2wY+?yGOI{K+&=U^4-tWNk zB0i{Pp$4wV*#H~ic>!s~p0k5*?9ACy=KHWwgWne#g#)hRHuSNiW1BwIz$nlMJM9C@ z+_(xh9J-CpoTE(raBFK|&1qUQs2^@^^QA^r@qT#Y-yHZRQa3>sU@kR#Je@wP*&w9s z;K2pVgxv(A2Aa1bJg+&^;&pghaOziW;|9x-^7R!CN<4tyKqVg>$Ct$G=aW1yWC88# z*Gq5t0RS)1njO^O_N%*@n68M&uNk*4qk25Zm)o|Y`YlfbKS!nih7TO5O|5EE9~@0; z(;=nlP@(CF+5{)nXrbxks`t^(=Jj27TW`0jEdxr+K%pg4*g5o+sdpcOldpBp-Tk-s z-yOO=w9&Qkexdc?>Kp&Db5AMQ{ID^kHufluJ%z?TwQ*2s94s^r8Rw#KKhT92R?jVu z8thYoeQ*N4{UCU#)X<)H+;8aKXxcpd4`8FVgzJ7o@5a&1%l8kA))IMGr8e^*_{#4) zptIk-fS5->HyjKva|HZnlKac8U4kh^7-XOehH(-c<%YgvZSlz!=Vg zz=y6bg%%}&EPHUbh4fex`~)!M^BkB0aKMQ2a0!ysZ?H6$;?zSgTS6&6!S<>)Grgkf z(!T-$3Gv9E4IOyfKey-4{Jlr+w&zc5Ha_59{qBCkgI9Mcp98Voejg!iY9 znQv?do#CgmY>V?O`}C;Y+4
+E9-J?(WnkNr=R-ML%us;)Z`-k}91m!{;S zktx}D^^*v04gJyBcq~Ty`irst9<0B%8VKBr6&U~2A87RlVEr{ze;0+9Pm+ivL?T2a zBe1AHQR}UK$rwEt1Kn@A6UHRHMt%cD9H4{&)F*b9W&dciv9s(SndmQS3+?X9;8pnlq9}^P$`s z#W7b-)56_KKngo(7gmsuFuMVwAPdwB6m9ma{VdR;@Pssm9b^F&^^gA&C)-`*tLNO| z<&f0Kc7V>ueV%jf>)i9Y^Y317GY8+fOZ|y=dpPcYu);ofx$xb)P`J%WoW!TNG#}y9 zj))`ej5yP-h%4=mxYG@hhIC`3F)c)dbW@}$-5hC7dm^5+7!lLnh&SC5Y2gvyk@BTm zBdxUTOzlaxMcQcDm1BYvK9a4&I^`vxa9$ov;hbNoohwZ@R}04t#uqw(UUgtR29 zhUenzFJ|94AG?-a#mdW8rQzcF*kw6oG+jKEO<&GJ2lQW9RhC~|Rn=_95HBvoE~n%& zo!08wSyfIWF*zp5XfrGpvD@trhMv6duS&m;Zi}Nd~gsRE}T3;gB z)fH$B@kRrTpUM&_IWDS@w#>=u)hxL(pGhn($|P=af6GvBqyY9$pvWx?o^~vYJgOb8X;|x1W22uCP+Q&+&g?koaaK#hPOPV zx#Tz-49CPn#*O~&`v#8t6@Qykv6X%s&Nc3Dq5q3=o#)p0H7;7gRvXJ56$t2cu{H@| z84Gc$m$k2PU+`ub`YROFJ*=ONcHN=2*S7%%Wmjxp7$nT7kCm<5mZsC9xeHdnqQFYY0amhdz#g<}tYlGF53)AN z#adw>{Hzu5M_Ad)lZUJ_EA2A+DC@U!HV*KMBROuhvyyt4l_WQVW%+J6Fwe@I88&cc znUi+Isv4<*!Loce92m)lGaiRkHBuvkW%(mUvf-B}z_K~9Dl9J9V(kzsC6BO@m9JHE z_A%D3PO(z*I4fCs@`P1pB{nwjRb@S}i*~cH;Rb+wno+WHMicJbbF5VoSSySaV6D}X zCHXvSwel*Q1FRLsoMmMzuaXm$n+A|CS`e&c$JGFNhV@%H8y$GScAdLWdfuYIN*1N6 z9cf~%up<>YP_rX1Gkhzr!Wm+%kKK{sD#&xJY~{PG2`D_Nz7h`exM9u~DPC(0rGSL$`&kLzkV1dLI7ZdSZ&zV>v zB};+vzo(Gkd?$O5NQwo4JI5KG2F`P3K$haG@8gH)M(UjWYPqcqS2_B zHiY3xC{bF584%RHMnj64Lwh9puN?fGNhxQdP zsM-`+y^sR5RPk{Oavs~ zq~LBvFRK}VOeNx10L7A`vyNcNxMses0;m? z&|ee|-0vOw=qK3PLtA?^p+^@+HDUDb*!iOH%KiS4kA8vOy|lYm6MA)FTocCcPRtgC zGgXZvnlPdZ6Phq_$FCor)ed9-{hq(V`1|n$V{U!}Q$=CA#G3MhdT>$;P8NkDl{NtD{7twh9Imt=)DbOs zq$nJ%wCzG{N~<5>v+yTKY#Zk2+3@JXeoffFnJo%o*uS3Or$Ox3gELxirqF!?R^!Fh zSdh}k=?~LIVHBW)Pi;L@I52~o*7tRAY)jI^r?v3uLU0zF`b$0g^`1jo&!IxkXn9x6 z!_bA0CWN*I{~`Q2%wz`XdpEphU*4q$rnCTV3XCumz8gMU7(9pc4tzaCH~gG-_*`L# zo@B7p8_;{lwBE5o?>NGkM;p|Hpn0@g3;Niz+Ss#2;kg~7%xHm`qHyAy2Cm6#j*q96 zcjo6ebN7Tp-vK9b@v<$4We)EE^-3n76?~FPAJfs-`87U?5Otr#SLqi%S@AzCuqALq ze_abz@j@&pHck?6N!_Pbe2^qc&brUe?x~s6qeiQEK^9S|+E?I{6`yD~2ypmZa}Q8_ zh8hglLLx024fEMm5|<6nX<1PdnX=@(K(cB!o=q99vzf�D(wdRTe_c1lMe|fD{A& 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 literal 0 HcmV?d00001 diff --git a/src/gui/widgets/__pycache__/vlan_bridge.cpython-312.pyc b/src/gui/widgets/__pycache__/vlan_bridge.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..320f5276ddba829276a5c94ef835c0278c5566a4 GIT binary patch literal 29391 zcmdsg3ve9AmDv6_Q;- z7M9|;640lM$dY41MXR)wRi{d zr>i)5uV;E@uow`ea&=X?A<@&*pV!^5-={yXdw=e7<(c3q8mS5W*I|?Czu*Ub?26}$ zZ-M8Ii8FELDbuuh%sg!wvrJpZtkbqJ+q8YmKJ6HDOy`W{OfzH5bnaO0blzCrv~$ck z&5p6tt})ki{#gFBd(1uU8S|JCj%BJ~x^S#;x@fG3d|RiAr@dodv&mw5#>CmaW8&;V z^M_Ws{IQZ74u8()_~G|RIp>}WahHQJ$$9SL(a81Ffg6!o{5Yw7IL@68Oa!N-+;fAG z>4^wLNX)sBYoQtRInNExMz0*5jm07}km4CWGJ8!9KNE~b1DAvH$B76(t-#JbcRDl^ zJkEt;5_@hmFfkQWYT(g-or(pgu^Pbu7vwL7f^%5>mEh!6gqI7&PlaN!An&(GmUA)5 zac(p;4PR!-dGYj-p^?eJ%uJAP45A&8-4Gl|JXv%qO!Wyon;o6%6O2B^~DwqL?DlSxrwpcyQ!D$ffTFQi7Eys~? zgd1sU?af(f3g{&~i)kJNFT9ceajuDn(&9o%WLCc|UXpx`BQaI4JUK$1% zZl!+h9q!Ql)W_v;%w@Ix&<^9Aoix4n4)Vw=nsb70*wKi_4ahbYYj&Pp_jrtNf zT8dad4W-zaXA;bIeP)Qm9TXDOY7g~m@9>jjVnEA9eYzSO^|_SKF-OpI;a|_xOCW1Id;nye2v1>-o|y%$^t$&qD6+53!JI8hs&p-^SK&@ zCe$9C&r9TOOQUCK-8eUe1~^k&4D}tNDYSPOMlIRT7W{fXI~`~rMO#4M=WRhp24!lw zVizb(&O<|?&TTYQ7hj-@Z#Rf9)WxgP%eYot5#Zt?g`vH5eP`Gjt{Cz?ucfEHZS#2{ z-xy7;y>h!r&4P_H-*hrsSL5o^J+iLpUNP6y~b=G02 zoWcey_0o92(iQ60-r=j7pZd58njczkoa@PPJ4K;tZyj{jh*orRzLKV;Z>W)B9;oLf ztpw_Of?NhT(d9Dkd$sSNHa4RcXrm}dVKtPUHmwwG^rh-?myUjR(3JD}TFQhbW!(w) zcJdTb7*W{faoXk)g(uq0J{cb7Ou(|YdR@woUa=4 z@w5)wd)s_9#<f2|LZjX^E>p{;Q7KnXL!r(`Xi2*G23`D7gZM^W zyh>Nbwc?tfUf-rLw70JB3|m7+4&TwzQ{T4vaOCihXlm`P%jY(frE32JehUCE$7o)F z_YbIFdv6#ysB;G@9p;O4da_xEEp?0zP`|4|qrRsYO}tL?Xm45$w4lhajfQ1vxpZ}I zp`n0@XJ`pf=YK-|+WRT$d`rtkeY6~?voZ8HX-e&_$NiQqHK?hsEMs|5q@~blMn4S& zT&S}eosKkxzo*rW`e>f{;zV)SNGon0cimC*Djq2BGz|mnEKeJrg-`D)qw`~nRpeeOC*RIP| zyanYO;6azmSSCESX92q|!?M&d6QnR6m8mmn&`T73`c+4DrUkwJBdwj(7yf6|r@gnB zb1Z2os882wW9a{arqteC$A(;GS`76WVnVm}rQ@eg3Txi0<07vbT9POU8*$9$apY#X zTWUB;RA-Vp3D-r-11|eIZ3W=_52#;zKZS(5ujQgXS`JV{sS%cR^N?QNQ*!luPgYO=jK*niDmn>db%nwx{)WlFcZjz@R6PH=5Fg}x zWCtlUbNO>S1b&`}jvJ%xXChp1Aie{4nS7)C?95ePi~Q<1=sPkw30rplc-?Ry8jW3n zoxICed@UDdVu8z-0VbrNe*c$cr)I9r#(bw{U=Qw6U^1wH@eh1iadtMA6{P?k_}mP2 z2>ljn5@$#aP8}PFm&&_czEi`#Cf~7>gTua&Apc5`k5?a`!OcN^7^E191!AE|NF8T; z_U`QL*}bQ;ue-A+?$UpC$MZLQcE^jgQd*t~#Da5y8~%Z~Q!XzS;9~>vT=H>zh8s8$ zcMbAENG0Pj7|%Zwn1QWKV!)$i%XXP-~` zp1Tr?1^30>M5W-c^XDOC$yfU&`{hn4EPVmqP}C$DOmpJpCGXp4o9H2qc^Sv z2Y_;~&P3*B;&qoIGqH}jVCeFd*nn>$GR3J~R39MEzR)co(@;A&{t3w*oxK+1C1!j) zG!u%Ak4u)BY2FLaCH9r6z|445-f5Pc=ots5f&FS87aJwV6xmAWafg(LcA6yTSst+h z^b1?%%*7xNgdCWXn5eRsE#(F{PTuw95pg^+uT&Tf#sIM~#OIj+e>pT0<&mMKJct;% z7MKJ!=N*_lhlEZ8hmTLFn~WDOx>M@e1e4SSwNxXu_#3k7gV1lp>7k54KP*#HqCN~+wci77xEfRpmj3x);g zcR;PqN9K5jRF_~;PQ*Zi&STXi29sk9k2}p$evn9laqi0GweiUbDMzj^r&gRq$Q9>Z zm?L+BY@v^bu1R?$GK!QdF~lF7;Up$>Z5%oF8lOw5p`@RP%>Y`aLO@QO5K@uSpeWRCGNu$%L`um*W~tKoBukzlH4=F80e6zIwaZUGMlFCpPf zNzOSOQ^-9bvGRwK$Vo&7{kQ~1K1z1}6rv{=xJ-#;sVxRb{d3jB--`0p}~$3D6=cdPj+3N z4Ry^CW2lEwu3UzzaWdjiI zT4mTboHv~!QzbA}i`+ZYZ%t>Ic1T@P_2vr;d23!@vLfwmU&vc!3Pq+-U>cKwR6*)U z>g5d61IdbO-aN3tfTz^A?CVaK^ei~nO6!vQ(xqJs>?%_%GBpBIlQd_TI`Y*lFwH4T z>gbYX>1c-O$BcDvPA@oDnIgFkKe&{9?rtQ*bgos@-)(~`y0MDxn{JWu35;*K?(p)_ zOUt#FGt3n=qD^4hL}r)3>{>b|_C720KAT}qs}P}8k+%>csX(>BR4@KchH2YaGzo?_ z)vi@G+}%ge2jE4f3~($m?E=#-GQ9%RyEH2H4GVq48Rp#Dj>fys5`;e}uUYmDq{|`S z>-lRHzUA5j>579`_jPPmg}_uS_P%rQt%Ipvv2CBwwlBl%C)HD84!w0KbxdqOAhaLI zFbAPxmGyU9H#I$3kUW-Q{A*>^i}7?B#zXIE;19fYAl2~Wt`E90%)mM#Uf7xDxX05m;$)t<@3%RRIfynF-m>rAfGEB{b>gMH^lj-VH*rH0(_DX@N6q$N~sTUbU zG1hTVXdlcl$4Fkp&0?`w-Y%54XPAx$<#ow}>GGX266%v@(-pngh_$lX<+`2evRzo? zbu1J!E|+&L75`!NPpUJ_VGUYU%JyUT2ks2BM+>V=a_>*y1G4PZnsq(Hw5*o-5aL6J zDc7ZtnLWcaJ*a3*M$#3#v1pY-kO)PF5T8wK84y|qGR!`W9%Apj`qrx%rVCIHm96bb zm+o9(*UIXXr_*IU3xH;?$g~Jd3sC;iCG%2GhUt6I(D6gk`?YJe?Wyr}?EyqwH9;I= zUM8C5j^{GW^SZz$foT$%PJ!u6#l)^7Lf4TDb95cyV-??XEI02-`}RT?k}?VJ*hJr6 zVRP7S!M7VYNW)127}+%<3Ya?Q@PcNg$Ch=|s7 zNr%FKDi;vK!n!8;)Xo&Yv@^r(egM1+jf5Kt6yUc+)jf4XAN zf_ts9Dfw!;vVWN=TSXawe2|3RYI;!LzT9yvU4Ivl$)gJt+SjTcnUKZGEBAgw>@cmg<%!kTT2a@46t7%GHD< zcBjjLRv%9iIvJKsq=2&5Bzks2gX%ZL4=!0{4oc@0i2loyxE7BOe zXU4%e7;qc65tli$o`l}m+7vcw<@ULZ+PPgW3unQ)a@MOYFn%|`J^^($YWp^DUjLR2 zsQ@RZSce$>zMt&mxAPlci}Sd>wg!K3^DhT12mf&rf24}Sh-bk1RD zjqjRCy6|zQvr($rv?|1MwNW2DFvXd19{!Ic(o;pT(TtVa zJlY`e*TIR`Z<4st5s$2t zP)x72CP!}X!tvFjvc;0y2NsU4vL2D$A+S3{wn1P)n`hWo z5>_R!RU+Fcu#HJB!?uyIT7j(zR>*CG%WM10cy6PzwJ&l5=QS@{O zo{ki^;@S1k1!X-fBvS)xJcyOqSf83r9(Xu*xFaF0k#X^BK0AgkhCMwnJb$ zQWGm|&qF&Tf0(DM1WqJVqbtnLzkcL46@9}TMM3cK{_+!hY=2sk1MUVLfYP1lfEHNk z{%SgaGbhZinE2@}_)rhekUBnT%XEiJpscT!>q+sUD+@LOnIXxEE5OR$U7RILj%0hTXF&h8 zbKnFI!rjDIfivo7_+EHRPGyr|GXGOMNVC$1Z8Fm+joVj^M5h@pC0V1bq64<8X>~U8X1*+xlOT{=F@!Amkr`$`%wRBt%tgsOp>&1u|GhvmkMsa& zM$;S_&EG)ZH_>??oDH}ggMfI;CUw5WzD)rl82ObTSY+h2JTjM>gJ~Agg7VacEQ;lj z$t!3^-Mswr3e&k6%}7l|C;b3CPeaRejsfFAQyBL&vz`Qo8_=q;q&AkR+5wLnCK@U|JdljN|E+u9T8jPQt3P@56WgD={+(-u zx%l@0`#9t!*ypkTWlGnG{6`SB5xwUjB;I6%Ub69{Y?sMjy~ND6aK>?i3K*{ zO<$TjKes^*c^^1&8(5ljgS)f4+n*y(q>5qxQJY8MCfudaWyy-Wl6GPsAz7z_Gm9V?#hhq)$C&BGiMn=jKiR8@UcG5#7*`d1R;TP-gk=VUz_M8AQEH{v2Sj};UH zSvuj%G#)S-P$%Ff4d5%Kd^6A;XPt+$^Yd_meje^{AQPcFP#jOX_yXlpF8u}!X<;eD z&N=2CGlu|%(J~C0_hdb4D{{6d*I+pX+M5FbL3UfB3Q2WTZ(MZsqn z*O{^&S_A&ukf$Om5YOmf|3t=It{KgybezEks8_4F<4HTppY^R4E8*)wDTP*uObU zFGFvbbfw_(P}YNVsxJlPqjN~IALsc9AK#%))}p@Y*=Wo+5%h7POP7LZw{?R5=KzaO zf+OWj@Z)F>MAm-3EX&hb{Q1BpYXSNrAv+>&}RcM$71d4=pPgv3npAz~-zcggbpJq&e_;Xse_-U$;` zxp9(pI(k`h%2gdFt9LoFg&C(ve%W-%iUv!$+&Il<=f)vp6g3;w1mrENgVPXfpbj>e zhjpPk_#QYgB-uQPE%1s3wL(FytQyhbt0iR%&UHF7uB>^d|E+$pvR|m|zwh|S zk*<8^=IMoJZjG!~)Zct|y`|Vra2*;n`n03)kI6qPtFT*NN_S!QGy5cj7YPi??4Cy)A;bCGBm!WmzrT@s9f~ zw^-ILl(nbJI&axmp~UIi(_%@BP|}hvX}x6y)!5MTe*e9Gv3|c$zyJQ%KdJj9xP0OH zbo~pU+HU18_I*~gW3{*xOcj>+C2qNLpHRH-_TYMP>Ees2md29(Y??_m9Tf)l2B7C$=JBea+)6Qo731vQ^o`YM4KwBo% zduoVOwu7#+-!en_Cc}Gy43^<(-HEm;Q=6oEj7G^c&phf_BMtYszCaC!YhYQ=W6MDe zXM`8^q>ctGVFO(b`f7u=%A0p4@<3ZTIIFDFiMDcpwsLT`gj3a44k*W`!J@u!J@skt ztffr@-Da3eKk|QKT}SZ^CuP(*fYEJ1KuK?s$tkqi3WW*h4%&EPc}g(gZyYG|1(I#e+{#;G{(c^b7Qf=&XW6 zHWBm+OV(E}K~%hQ)AU8wxmfUXsI0y#D=Tlg=*^x#Qw3Hpxa-sIMp%U{Diw>GgrX*~ zs7)wpOBZ!4Fhqg*g#y1=&?gl1r3?BO9KUpz-z)y$`JZ0Q(qOPUdlofVY2}@{+jC-R zyHMJm8oOV7|N18->C)kw>_X11qQ6iSPP5={7TrC9yC>t`1qw%2Hlnvf@OGrVT}0)` z>P9T<5z2bNz>FvzS>cE!9YRS*x}7xMsX*U0GzS1-4qh zF@>r~-oQc?m0=qR5;S#{t(um1p8jOyljFeH>aw#Uk$LZv(-gOtSd#5(r77eUbv& z0`0A>a1-?elS5F9Ejqjy>@tH;vVmGtg&34(FlEz3A_zGR&jwR8fM?gkBg-R>p}1!d zmpb@qIJ^R9iohN{-g1Nw`rsNyUvyUS%mrp*D9q%O1U^|l_?qLL&4=goztKx~5gcC1`Cng2>-#Gb&-BJ`ZFXTLAOl;Bj zu8JkyLP@t+(kqnoe(LO12g=<-W%p7^x^j;^NdC-SvW6=$&4Q;nWlM$b7iByL$X+yt zta$tnFt$PPz|MB6e`#>Vv*)49#P)6GaYPN_@gIG49)IFp&@3*nfDMg>|A#==Mnh60 zimV5&MTA=7L-s=rINNA3GHX})2{~5dE2y6VcN+1cekVGn4l#``j3m6RF0_yVik9AB zAW3+4Lwc|d89hOcTjQP+=n^`$?OkHwJ*kmg+TGj@QWmI=# zh}I1dZRLRQ|3`@S4G}Qa43l4+rf#%!IVqvRL*qZ7~7xpaV5VKHu>lL?X-;1XD ze+cIMy=!o!0<4?c9+|AUBj%f@7LF{uqGKmwa`1AhHq&w7{-uvD{B%0qaXQ1E*)qlG zPp8ry&t}-uTc#Kj4i2X~&aJTL|1t-Q+^nmR{|FC00{5$oM>#VYw}Bz2k=+hIY#jc$ z2|3Y2P_67ItZtcUFylx>rRL*u15=}zOc%_Gz2UW zRclJpE<9kE^%&MrXTJ#bGs>mvHVvG1G4Pf++IZxHL>b3yjnpW zZNN1UXjep@Eoj7OA;)M@#&B7M$~l4w)TvodxGpOIKEuY)8#brh7P`?!gE91$uK*p# zAz9BBb#Ko~2A^8p$yRPWcLb*xc%bn_+=+g;1paboXQzDkM2YUaJ*L5(1c-d>nfqXO z5U)w#vB9u3!^2BnkipvU{|%izwnIsl; zaC(`hAmNIM9FNLogJDk;0=8VI!4i_FP%eW53dQMuy5xFTaT5~NmG>cJ5NyOQl|4db&r&1I ztnkze9Jf+Uj!_$G$K{R*UB@!aaa}wXDKa|+X6I6`xa+jA>vV=Wql@1zKN>%YOqalP zE!o8GqeA!56=qOYdaAI2?m=-tl-^135Tysbk^P{lE!{6BLIiVUJ=%l@SjjT zYgPy*Rq!cpsn-@Q!gV$-z0U`ItID7LP|`{q0KvC z?5iM`pb6n*J$f|Bc3f8ew~$7%Mq@nxf57LL&9(Rp_<5L#W|#vB4G)3^p@x>`yUjH0GftB*@4M&S>;(0oy6pQttw6t2U=Gn!nI`Ycmcox3HQ` zGiEq{VU7Oo7G*uA*SBoD(VS)DXvqlY#;vkwtNBWBOiJH)mXay@{dl%YI_!?Hm;Ri(ic6fU)@HTGd z8d0hdZ^O^gJPB_D&j6>AnUxjz&-{8HoWrt{jYZ4r`+yVS>*_HepIi>Mgo&A+70j6! zFlVYIaWJanb6~?53}zra55m1_xSewIe7!u(S35tQTdLuHtmWtNGT6a<7Ik~BQU6iOUY2d_~Z5R&1MP=Rilsc zflHU*lCrZiQ#WXdLAZlVxrEBs-ax(7~-nIJ$-xm_?Bw zacL1Ad36z0uff2=Xn68x04(|iqzCCyx&tl)@s=#)Juns__FNw@cX8RBv$xNR#VtZ{ zOR6XpNEdgD#eG6?U%I#-j*YpCm&@8db+@h7)Zb>;Jr#?A+wR+W51o+tVZMniSu}t5 z;A*iy)tcI|^xSgc0fbm}^Z2T(bg}2V@v`Rz@ruocOzFOF?#I6^%5Z}Ua zvMeK8$&<74zq7P!sd2dxkMNVBk?w48$1|VUKbrd_u-rDh!kpVIRB)g`1`EFro~Kzh z$y#oQ+}V%OpyCOZISho2e(4;>r?#O{z7a|zR%dWbflSLLF+d|MGa;-f^tDMLk-rA7 zfpVd9Av%BhA4nb`4CSGh?sP{nT(JPy)$SfxCP(JgATk|+VS4lCVY({0aa8#K1Cn)y zf(9~>>`$ma*oG@le!IR)TQ}2wB6HSXV<$cP zl{*P;Gg!kLYLHJBN0a@j!4;-^bGM-?B)sw7W4ev+1tkOV+0Wx_hxv*v@;qkn2@1*l z_{Bzgw8X~a>I51&o|o+{$wCEOYzGFG*JA4IEcgnpDq|ZJAu8ofPDP@@;}~u*@7E&# z2Cf^$k~k z!L@~G0}0m_=8fa+ezRc4IWDo|<1fz!rsNoM^%OZV%5YFL@`_23t5Jlw2 zVOhnLVp4P%jF5KJlbKkw?4|V+Pfa*S?tf5A2AmD zC2-yLo=0^}_P$5M<{JB``O#6c+rIZvofAST82gC%QE{>TnE6q=#omBfz;W5XW`0y% zWj}9zbkJ&Vc~oe#_xvx`YA+>(N@_CXFO}TGH_pX&cb<#MS2ITCyBTd*S)?ZI?nQE= zA-PYGT+k?UB8y3IIK<=5r#yzo^7OVNZ`8TF)_X|0MF_|0w Wld0oZwpVh@rMKpOZNfJJ{eJ 500: + self.log_output.append(f"\n[INFO] Large range ({len(target_ports)} ports). Hiding closed ports.") + else: + self.table.setSortingEnabled(False) + + for port in target_ports: + for proto in protos: + if (port, proto) not in existing_ports: + # Pass update_filter=False for bulk addition + self.add_result_row(port, proto, "closed", "", update_filter=False) + + self.table.setSortingEnabled(True) + + self.apply_filters() # Apply once at the end + + finally: + self.table.setUpdatesEnabled(True) + + def row_exists(self, port, proto): + # Deprecated / Unused in optimized version but kept for safety if needed elsewhere + items = self.table.findItems(str(port), Qt.MatchFlag.MatchExactly) + for item in items: + if item.column() == 0: + if self.table.item(item.row(), 1).text() == proto: + return True + return False + + def update_or_add_row(self, port, proto, state, service): + # Find row + items = self.table.findItems(str(port), Qt.MatchFlag.MatchExactly) + found_row = -1 + for item in items: + if item.column() == 0: + if self.table.item(item.row(), 1).text() == proto: + found_row = item.row() + break + + if found_row >= 0: + # Update + self.update_row_style(found_row, state) + self.table.setItem(found_row, 3, QTableWidgetItem(service)) + else: + # Add + self.add_result_row(port, proto, state, service) + + def add_result_row(self, port, proto, state, service, update_filter=True): + row = self.table.rowCount() + self.table.insertRow(row) + self.table.setItem(row, 0, QTableWidgetItem(str(port))) + self.table.setItem(row, 1, QTableWidgetItem(proto)) + + state_item = QTableWidgetItem(state) + self.table.setItem(row, 2, state_item) + self.table.setItem(row, 3, QTableWidgetItem(service)) + + self.update_row_style(row, state) + + if update_filter: + self.apply_filters() # Re-apply filters to new row + + def update_row_style(self, row, state): + state_item = self.table.item(row, 2) + if state == 'open': + state_item.setForeground(Qt.GlobalColor.green) + state_item.setText("open") + elif state == 'closed': + state_item.setForeground(Qt.GlobalColor.red) + state_item.setText("closed") + else: + state_item.setForeground(Qt.GlobalColor.yellow) + state_item.setText(state) + + def apply_filters(self): + filter_state = self.filter_combo.currentText().lower() + search_text = self.search_input.text().strip() + + for i in range(self.table.rowCount()): + show = True + + # State Filter + state_item = self.table.item(i, 2) + state = state_item.text() + if filter_state != "all": + if filter_state == "open" and state != "open": + show = False + elif filter_state == "closed" and state != "closed": + show = False + + # Search Filter + if show and search_text: + port_item = self.table.item(i, 0) + if search_text not in port_item.text(): + show = False + + self.table.setRowHidden(i, not show) + + def on_scan_error(self, msg): + self.chain_udp = False # Stop chain on error + self.scan_btn.setEnabled(True) + self.scan_btn.setText("Start Port Scan") + self.scan_btn.setStyleSheet("") + + if "kill" in str(msg) or "None" in str(msg) or "stopped by user" in str(msg): + return + + QMessageBox.critical(self, "Error", f"Scan failed: {msg}") + + def closeEvent(self, event): + if self.scan_thread and self.scan_thread.isRunning(): + self.scan_thread.stop() + event.accept() diff --git a/src/gui/widgets/scanner_view.py b/src/gui/widgets/scanner_view.py new file mode 100644 index 0000000..8361031 --- /dev/null +++ b/src/gui/widgets/scanner_view.py @@ -0,0 +1,163 @@ +from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, + QLineEdit, QPushButton, QTableWidget, QTableWidgetItem, + QHeaderView, QMessageBox, QProgressBar, QComboBox, QTextEdit, QGroupBox) +from src.core.scanner import NetworkScanner + +class ScannerWidget(QWidget): + def __init__(self): + super().__init__() + self.scanner = NetworkScanner() + self.scan_thread = None + + self.layout = QVBoxLayout(self) + + # Controls + controls_layout = QHBoxLayout() + + self.target_input = QLineEdit() + self.target_input.setPlaceholderText("Target Network (e.g. 192.168.1.0/24)") + + self.scan_btn = QPushButton("Scan Network") + self.scan_btn.clicked.connect(self.start_scan) + + self.filter_combo = QComboBox() + self.filter_combo.addItems(["All", "Up", "Down"]) + self.filter_combo.currentTextChanged.connect(self.apply_filters) + + controls_layout.addWidget(QLabel("Target:")) + controls_layout.addWidget(self.target_input) + controls_layout.addWidget(QLabel("Filter:")) + controls_layout.addWidget(self.filter_combo) + controls_layout.addWidget(self.scan_btn) + + self.layout.addLayout(controls_layout) + + # Progress Bar + self.progress = QProgressBar() + self.progress.setRange(0, 0) # Indeterminate + self.progress.hide() + self.layout.addWidget(self.progress) + + # Results Table + self.table = QTableWidget() + self.table.setColumnCount(5) + self.table.setHorizontalHeaderLabels(["IP", "MAC", "Vendor", "Status", "Open Ports"]) + self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) + self.layout.addWidget(self.table) + + # Progress Log + log_group = QGroupBox("Scan Progress") + log_layout = QVBoxLayout(log_group) + self.log_output = QTextEdit() + self.log_output.setReadOnly(True) + self.log_output.setMaximumHeight(150) + self.log_output.setStyleSheet("background-color: #1e1e1e; color: #00ff00; font-family: monospace;") + log_layout.addWidget(self.log_output) + self.layout.addWidget(log_group) + + def start_scan(self): + if self.scan_btn.text() == "Stop Scan": + if self.scan_thread: + self.scan_thread.stop() + self.scan_btn.setEnabled(False) + self.scan_btn.setText("Stopping...") + return + + target = self.target_input.text() + + if not target: + QMessageBox.warning(self, "Error", "Please enter a target IP range.") + return + + self.scan_btn.setText("Stop Scan") + self.scan_btn.setStyleSheet("background-color: #800000; color: white;") + self.progress.show() + self.table.setRowCount(0) + self.log_output.clear() + + # Use default scan (TCP, Fast ports) for discovery + # skip_discovery=False ensures we ONLY scan hosts that are UP (ARP/Ping response) + self.scan_thread = self.scanner.scan(target, ports=None, scan_type="TCP", skip_discovery=False) + self.scan_thread.scan_finished.connect(self.on_scan_finished) + self.scan_thread.scan_error.connect(self.on_scan_error) + self.scan_thread.scan_progress.connect(self.on_scan_progress) + + def on_scan_progress(self, msg): + self.log_output.append(msg) + sb = self.log_output.verticalScrollBar() + sb.setValue(sb.maximum()) + + def on_scan_finished(self, result): + self.scan_btn.setEnabled(True) + self.scan_btn.setText("Scan Network") + self.scan_btn.setStyleSheet("") + self.progress.hide() + + scan_data = result.get('scan', {}) + self.table.setRowCount(len(scan_data)) + + for i, (ip, data) in enumerate(scan_data.items()): + mac = "Unknown" + vendor = "Unknown" + status = data['status']['state'] + + if 'addresses' in data and 'mac' in data['addresses']: + mac = data['addresses']['mac'] + if 'vendor' in data and mac in data['vendor']: + vendor = data['vendor'][mac] + + # Collect Open Ports + open_ports = [] + if 'tcp' in data: + for port, info in data['tcp'].items(): + if info['state'] == 'open': + open_ports.append(f"{port}/tcp") + if 'udp' in data: + for port, info in data['udp'].items(): + if info['state'] == 'open': + open_ports.append(f"{port}/udp") + + ports_str = ", ".join(open_ports) if open_ports else "" + + self.table.setItem(i, 0, QTableWidgetItem(ip)) + self.table.setItem(i, 1, QTableWidgetItem(mac)) + self.table.setItem(i, 2, QTableWidgetItem(vendor)) + self.table.setItem(i, 3, QTableWidgetItem(status)) + self.table.setItem(i, 4, QTableWidgetItem(ports_str)) + + self.apply_filters() + + def apply_filters(self): + filter_text = self.filter_combo.currentText().lower() + + for row in range(self.table.rowCount()): + status_item = self.table.item(row, 3) + if not status_item: + continue + + status = status_item.text().lower() + + if filter_text == "all": + self.table.setRowHidden(row, False) + elif filter_text == "up" and status == "up": + self.table.setRowHidden(row, False) + elif filter_text == "down" and status == "down": + self.table.setRowHidden(row, False) + else: + self.table.setRowHidden(row, True) + + def on_scan_error(self, error): + self.scan_btn.setEnabled(True) + self.scan_btn.setText("Scan Network") + self.scan_btn.setStyleSheet("") + self.progress.hide() + + if "stopped by user" in str(error): + return + + QMessageBox.critical(self, "Scan Error", error) + + def closeEvent(self, event): + if self.scan_thread and self.scan_thread.isRunning(): + self.scan_thread.stop() + event.accept() diff --git a/src/gui/widgets/sniffer_view.py b/src/gui/widgets/sniffer_view.py new file mode 100644 index 0000000..bcf8da0 --- /dev/null +++ b/src/gui/widgets/sniffer_view.py @@ -0,0 +1,162 @@ +from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, + QComboBox, QPushButton, QTableWidget, QTableWidgetItem, + QHeaderView, QLineEdit, QMessageBox, QCheckBox, QSplitter, QGroupBox) +from PyQt6.QtGui import QColor +from PyQt6.QtCore import Qt +from src.core.sniffer import NetworkSniffer + +class SnifferWidget(QWidget): + def __init__(self, network_manager): + super().__init__() + self.nm = network_manager + self.sniffer = NetworkSniffer() + self.is_sniffing = False + + self.layout = QVBoxLayout(self) + + # Controls + controls_layout = QHBoxLayout() + + self.iface_combo = QComboBox() + self.refresh_interfaces() + + self.filter_input = QLineEdit() + self.filter_input.setPlaceholderText("BPF Filter (e.g. tcp port 80)") + + self.ip_filter_input = QLineEdit() + self.ip_filter_input.setPlaceholderText("Filter IP (Src/Dst)") + self.ip_filter_input.textChanged.connect(self.apply_ip_filter) + + self.start_btn = QPushButton("Start Sniffing") + self.start_btn.clicked.connect(self.toggle_sniffing) + + controls_layout.addWidget(QLabel("Interface:")) + controls_layout.addWidget(self.iface_combo) + controls_layout.addWidget(QLabel("BPF:")) + controls_layout.addWidget(self.filter_input) + controls_layout.addWidget(QLabel("IP Filter:")) + controls_layout.addWidget(self.ip_filter_input) + controls_layout.addWidget(self.start_btn) + + self.layout.addLayout(controls_layout) + + # Splitter for Dual Consoles + self.splitter = QSplitter(Qt.Orientation.Vertical) + self.layout.addWidget(self.splitter) + + # Top Console: All Traffic + self.all_traffic_group = QGroupBox("All Traffic") + self.all_traffic_layout = QVBoxLayout(self.all_traffic_group) + self.table_all = self.create_table() + self.all_traffic_layout.addWidget(self.table_all) + self.splitter.addWidget(self.all_traffic_group) + + # Bottom Console: Failed / Slow / Anomalies + self.failed_traffic_group = QGroupBox("Failed / Slow / Anomalies") + self.failed_traffic_layout = QVBoxLayout(self.failed_traffic_group) + self.table_failed = self.create_table() + self.failed_traffic_layout.addWidget(self.table_failed) + self.splitter.addWidget(self.failed_traffic_group) + + # Set initial sizes + self.splitter.setSizes([400, 200]) + + def create_table(self): + table = QTableWidget() + table.setColumnCount(6) + table.setHorizontalHeaderLabels(["Time", "Source", "Destination", "Protocol", "Info", "Status"]) + table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Interactive) + table.horizontalHeader().setStretchLastSection(True) + table.setAlternatingRowColors(True) + return table + + def refresh_interfaces(self): + self.iface_combo.clear() + interfaces = self.nm.list_interfaces() + for iface in interfaces: + self.iface_combo.addItem(iface['name']) + + def toggle_sniffing(self): + if self.is_sniffing: + self.sniffer.stop_sniffing() + self.start_btn.setText("Start Sniffing") + self.start_btn.setStyleSheet("background-color: #007acc;") + self.is_sniffing = False + else: + iface = self.iface_combo.currentText() + filter_str = self.filter_input.text() + + self.table_all.setRowCount(0) + self.table_failed.setRowCount(0) + + self.sniffer_thread = self.sniffer.start_sniffing(iface, filter_str) + self.sniffer_thread.packet_received.connect(self.add_packet) + self.sniffer_thread.error_occurred.connect(self.on_error) + + self.start_btn.setText("Stop Sniffing") + self.start_btn.setStyleSheet("background-color: #d32f2f;") + self.is_sniffing = True + + def add_packet(self, pkt): + # Add to All Traffic + self.insert_packet_row(self.table_all, pkt) + + # Check if failed/slow/anomaly + status = pkt.get('status', '') + if status in ['Error', 'Connecting']: + self.insert_packet_row(self.table_failed, pkt) + + def insert_packet_row(self, table, pkt): + row = table.rowCount() + table.insertRow(row) + table.setItem(row, 0, QTableWidgetItem(pkt['time'])) + table.setItem(row, 1, QTableWidgetItem(pkt['src'])) + table.setItem(row, 2, QTableWidgetItem(pkt['dst'])) + table.setItem(row, 3, QTableWidgetItem(str(pkt['proto']))) + table.setItem(row, 4, QTableWidgetItem(pkt['info'])) + + status_item = QTableWidgetItem(pkt.get('status', '')) + if pkt.get('status') == 'Error': + status_item.setForeground(QColor('#ff5252')) # Red + elif pkt.get('status') == 'Connecting': + status_item.setForeground(QColor('#ffab40')) # Orange + elif pkt.get('status') == 'OK': + status_item.setForeground(QColor('#69f0ae')) # Green + + table.setItem(row, 5, status_item) + + # Check filter immediately + self.check_filter_for_row(table, row, pkt['src'], pkt['dst']) + + if not table.isRowHidden(row): + table.scrollToBottom() + + def apply_ip_filter(self): + filter_ip = self.ip_filter_input.text().strip() + + for table in [self.table_all, self.table_failed]: + for row in range(table.rowCount()): + src = table.item(row, 1).text() + dst = table.item(row, 2).text() + + if not filter_ip: + table.setRowHidden(row, False) + elif filter_ip in src or filter_ip in dst: + table.setRowHidden(row, False) + else: + table.setRowHidden(row, True) + + def check_filter_for_row(self, table, row, src, dst): + filter_ip = self.ip_filter_input.text().strip() + if not filter_ip: + table.setRowHidden(row, False) + return + + if filter_ip in src or filter_ip in dst: + table.setRowHidden(row, False) + else: + table.setRowHidden(row, True) + + def on_error(self, msg): + self.toggle_sniffing() + QMessageBox.critical(self, "Sniffer Error", msg) diff --git a/src/gui/widgets/vlan_bridge.py b/src/gui/widgets/vlan_bridge.py new file mode 100644 index 0000000..a500eb6 --- /dev/null +++ b/src/gui/widgets/vlan_bridge.py @@ -0,0 +1,510 @@ +from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, + QComboBox, QSpinBox, QPushButton, QGroupBox, + QMessageBox, QFormLayout, QLineEdit, QTableWidget, + QTableWidgetItem, QHeaderView, QCheckBox, QSplitter) +from PyQt6.QtCore import Qt, QTimer +from src.core.vlan_scanner import VLANScanner + +class VLANBridgeWidget(QWidget): + def __init__(self, network_manager): + super().__init__() + self.nm = network_manager + self.vlan_scanner = VLANScanner() + self.scan_thread = None + + self.layout = QVBoxLayout(self) + + # Create a horizontal layout for the top part (Bridge + Wifi) + self.splitter = QSplitter(Qt.Orientation.Vertical) + self.layout.addWidget(self.splitter) + + # Top Container Widget + top_widget = QWidget() + top_layout = QHBoxLayout(top_widget) + top_layout.setContentsMargins(0, 0, 0, 0) + + # Left Column: Wired Bridging + left_col = QVBoxLayout() + + # Wired Bridging Group + wired_group = QGroupBox("Wired Bridging") + wired_layout = QVBoxLayout(wired_group) + wired_layout.setSpacing(5) + wired_layout.setContentsMargins(10, 15, 10, 10) + + # Mode Selection + mode_layout = QHBoxLayout() + mode_layout.addWidget(QLabel("Mode:")) + self.mode_combo = QComboBox() + self.mode_combo.addItems(["VLAN Trunk (Trunk -> Access)", "Passthrough (Untagged -> Untagged)"]) + self.mode_combo.currentIndexChanged.connect(self.toggle_mode_inputs) + mode_layout.addWidget(self.mode_combo) + wired_layout.addLayout(mode_layout) + + # Interface Selection + form_layout = QFormLayout() + form_layout.setSpacing(5) + self.trunk_combo = QComboBox() # Input + self.trunk_combo.currentIndexChanged.connect(self.on_trunk_combo_changed) + self.access_combo = QComboBox() # Output + + self.lbl_input = QLabel("Input Interface (Trunk):") + self.lbl_output = QLabel("Output Interface (Access):") + + form_layout.addRow(self.lbl_input, self.trunk_combo) + form_layout.addRow(self.lbl_output, self.access_combo) + + # VLAN ID Input + self.vlan_input = QSpinBox() + self.vlan_input.setRange(1, 4094) + self.lbl_vlan = QLabel("VLAN ID:") + form_layout.addRow(self.lbl_vlan, self.vlan_input) + + wired_layout.addLayout(form_layout) + + # IP / DHCP Configuration + ip_group = QGroupBox("Bridge IP & DHCP Server") + ip_layout = QFormLayout(ip_group) + ip_layout.setSpacing(5) + + self.enable_dhcp_cb = QCheckBox("Enable DHCP Server & Static IP") + self.enable_dhcp_cb.toggled.connect(self.toggle_dhcp_inputs) + ip_layout.addRow(self.enable_dhcp_cb) + + self.bridge_ip = QLineEdit("192.168.50.1") + self.dhcp_start = QLineEdit("192.168.50.10") + self.dhcp_end = QLineEdit("192.168.50.100") + + ip_layout.addRow("Bridge IP (Gateway):", self.bridge_ip) + ip_layout.addRow("DHCP Start:", self.dhcp_start) + ip_layout.addRow("DHCP End:", self.dhcp_end) + + wired_layout.addWidget(ip_group) + self.ip_group = ip_group + + # Initialize inputs state + self.toggle_dhcp_inputs(False) + + self.bridge_btn = QPushButton("Create Bridge") + self.bridge_btn.clicked.connect(self.create_bridge) + wired_layout.addWidget(self.bridge_btn) + + # Bridge Management Group + manage_group = QGroupBox("Manage Bridges") + manage_layout = QHBoxLayout(manage_group) + manage_layout.setContentsMargins(5, 15, 5, 5) + + self.active_bridges_combo = QComboBox() + self.delete_bridge_btn = QPushButton("Delete Bridge") + self.delete_bridge_btn.setStyleSheet("background-color: #800000; color: white;") + self.delete_bridge_btn.clicked.connect(self.delete_bridge) + + manage_layout.addWidget(QLabel("Active Bridges:")) + manage_layout.addWidget(self.active_bridges_combo) + manage_layout.addWidget(self.delete_bridge_btn) + + wired_layout.addWidget(manage_group) + + left_col.addWidget(wired_group) + top_layout.addLayout(left_col) + + # Right Column: Wi-Fi Bridging + right_col = QVBoxLayout() + wifi_group = QGroupBox("Wi-Fi AP Bridging") + wifi_layout = QVBoxLayout(wifi_group) + wifi_layout.setSpacing(5) + wifi_layout.setContentsMargins(10, 15, 10, 10) + + wifi_form = QFormLayout() + wifi_form.setSpacing(5) + self.wifi_iface_combo = QComboBox() + self.wifi_iface_combo.currentIndexChanged.connect(self.on_wifi_iface_changed) + self.ssid_input = QLineEdit() + self.password_input = QLineEdit() + self.password_input.setEchoMode(QLineEdit.EchoMode.Password) + self.bridge_select_combo = QComboBox() # Select which bridge to attach to + + wifi_form.addRow("Wi-Fi Interface:", self.wifi_iface_combo) + wifi_form.addRow("SSID:", self.ssid_input) + wifi_form.addRow("Password:", self.password_input) + wifi_form.addRow("Attach to Bridge:", self.bridge_select_combo) + + wifi_layout.addLayout(wifi_form) + + # Wi-Fi DHCP Configuration + wifi_dhcp_group = QGroupBox("Wi-Fi DHCP Server") + wifi_dhcp_layout = QFormLayout(wifi_dhcp_group) + wifi_dhcp_layout.setSpacing(5) + + self.wifi_enable_dhcp_cb = QCheckBox("Enable DHCP on Bridge") + self.wifi_enable_dhcp_cb.toggled.connect(self.toggle_wifi_dhcp_inputs) + wifi_dhcp_layout.addRow(self.wifi_enable_dhcp_cb) + + self.wifi_bridge_ip = QLineEdit("192.168.60.1") + self.wifi_dhcp_start = QLineEdit("192.168.60.10") + self.wifi_dhcp_end = QLineEdit("192.168.60.100") + + wifi_dhcp_layout.addRow("Bridge IP:", self.wifi_bridge_ip) + wifi_dhcp_layout.addRow("DHCP Start:", self.wifi_dhcp_start) + wifi_dhcp_layout.addRow("DHCP End:", self.wifi_dhcp_end) + + wifi_layout.addWidget(wifi_dhcp_group) + self.wifi_dhcp_group = wifi_dhcp_group + + # Initialize Wi-Fi DHCP inputs state + self.toggle_wifi_dhcp_inputs(False) + + wifi_btns_layout = QHBoxLayout() + self.wifi_btn = QPushButton("Start Wi-Fi AP") + self.wifi_btn.clicked.connect(self.start_wifi) + wifi_btns_layout.addWidget(self.wifi_btn) + + self.stop_wifi_btn = QPushButton("Stop Wi-Fi AP") + self.stop_wifi_btn.clicked.connect(self.stop_wifi) + self.stop_wifi_btn.setEnabled(False) + wifi_btns_layout.addWidget(self.stop_wifi_btn) + + wifi_layout.addLayout(wifi_btns_layout) + + # Connected Clients + self.clients_group = QGroupBox("Connected Clients") + clients_layout = QVBoxLayout(self.clients_group) + clients_layout.setContentsMargins(5, 15, 5, 5) + self.clients_table = QTableWidget() + self.clients_table.setColumnCount(3) + self.clients_table.setHorizontalHeaderLabels(["MAC", "IP", "Hostname"]) + self.clients_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) + self.clients_table.setMaximumHeight(150) + clients_layout.addWidget(self.clients_table) + wifi_layout.addWidget(self.clients_group) + + right_col.addWidget(wifi_group) + top_layout.addLayout(right_col) + + # Client Refresh Timer + self.client_timer = QTimer() + self.client_timer.timeout.connect(self.refresh_clients) + self.client_timer.setInterval(5000) # 5 seconds + + self.splitter.addWidget(top_widget) + + # Bottom Part: VLAN Scanner + scanner_group = QGroupBox("Passive VLAN Scanner") + scanner_layout = QVBoxLayout(scanner_group) + scanner_layout.setContentsMargins(10, 15, 10, 10) + + scan_controls = QHBoxLayout() + self.scan_iface_combo = QComboBox() + self.scan_btn = QPushButton("Start Scan (10s)") + self.scan_btn.clicked.connect(self.start_scan) + + scan_controls.addWidget(QLabel("Interface:")) + scan_controls.addWidget(self.scan_iface_combo) + scan_controls.addWidget(self.scan_btn) + + scanner_layout.addLayout(scan_controls) + + self.scan_table = QTableWidget() + self.scan_table.setColumnCount(2) + self.scan_table.setHorizontalHeaderLabels(["VLAN ID", "Packets Detected"]) + self.scan_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) + self.scan_table.setMinimumHeight(200) # Ensure it has space + scanner_layout.addWidget(self.scan_table) + + self.port_type_label = QLabel("Port Type: Unknown") + self.port_type_label.setStyleSheet("font-weight: bold; color: #aaaaaa;") + scanner_layout.addWidget(self.port_type_label) + + self.splitter.addWidget(scanner_group) + + # Set initial sizes for splitter (Top, Bottom) + self.splitter.setSizes([400, 300]) + + self.refresh_interfaces() + self.toggle_mode_inputs() # Initialize UI based on default mode + + def refresh_interfaces(self): + interfaces = self.nm.list_interfaces() + self.trunk_combo.clear() + self.access_combo.clear() + self.scan_iface_combo.clear() + self.wifi_iface_combo.clear() + + for iface in interfaces: + name = iface['name'] + self.trunk_combo.addItem(name) + self.access_combo.addItem(name) + self.scan_iface_combo.addItem(name) + if iface['type'] == 'Wireless': + self.wifi_iface_combo.addItem(name) + + # Populate bridges + self.bridge_select_combo.clear() + self.active_bridges_combo.clear() + + bridges = [iface['name'] for iface in interfaces if iface['name'].startswith('br')] + + for br in bridges: + self.bridge_select_combo.addItem(br) + self.active_bridges_combo.addItem(br) + + # Add manual entry option for Wi-Fi bridge selection if needed + self.bridge_select_combo.setEditable(True) + + def toggle_mode_inputs(self): + mode = self.mode_combo.currentText() + if "Passthrough" in mode: + self.lbl_input.setText("Interface 1:") + self.lbl_output.setText("Interface 2:") + self.lbl_vlan.setVisible(False) + self.vlan_input.setVisible(False) + else: + self.lbl_input.setText("Input Interface (Trunk):") + self.lbl_output.setText("Output Interface (Access):") + self.lbl_vlan.setVisible(True) + self.lbl_vlan.setVisible(True) + self.vlan_input.setVisible(True) + + def toggle_dhcp_inputs(self, checked): + self.bridge_ip.setEnabled(checked) + self.dhcp_start.setEnabled(checked) + self.dhcp_end.setEnabled(checked) + + def toggle_wifi_dhcp_inputs(self, checked): + self.wifi_bridge_ip.setEnabled(checked) + self.wifi_dhcp_start.setEnabled(checked) + self.wifi_dhcp_end.setEnabled(checked) + + def on_trunk_combo_changed(self, index): + iface_name = self.trunk_combo.currentText() + self.autofill_dhcp(iface_name, self.bridge_ip, self.dhcp_start, self.dhcp_end) + + def on_wifi_iface_changed(self, index): + iface_name = self.wifi_iface_combo.currentText() + self.autofill_dhcp(iface_name, self.wifi_bridge_ip, self.wifi_dhcp_start, self.wifi_dhcp_end) + + def autofill_dhcp(self, iface_name, ip_field, start_field, end_field): + if not iface_name: + return + + # Get config to auto-fill DHCP + config = self.nm.get_interface_config(iface_name) + ip = config.get('ip', '') + + if ip: + # Suggest a subnet based on this IP + parts = ip.split('.') + if len(parts) == 4: + base = ".".join(parts[:3]) + ip_field.setText(ip) + start_field.setText(f"{base}.100") + end_field.setText(f"{base}.200") + + def create_bridge(self): + mode = self.mode_combo.currentText() + iface1 = self.trunk_combo.currentText() + iface2 = self.access_combo.currentText() + + if iface1 == iface2: + QMessageBox.warning(self, "Error", "Interfaces must be different") + return + + success = False + msg = "" + bridge_name = "" + + if "Passthrough" in mode: + success, msg = self.nm.create_simple_bridge(iface1, iface2) + bridge_name = "br_pass" + else: + vlan_id = self.vlan_input.value() + success, msg = self.nm.create_vlan_bridge(iface1, vlan_id, iface2) + bridge_name = f"br{vlan_id}" + + if success: + # Handle IP/DHCP + if self.enable_dhcp_cb.isChecked(): + ip = self.bridge_ip.text() + start = self.dhcp_start.text() + end = self.dhcp_end.text() + + # Start DHCP + d_success, d_msg = self.nm.start_dhcp_server(bridge_name, ip, start, end) + if d_success: + msg += f"\nDHCP Server running on {ip}" + else: + msg += f"\nDHCP Error: {d_msg}" + + QMessageBox.information(self, "Success", msg) + self.bridge_select_combo.addItem(bridge_name) + else: + QMessageBox.critical(self, "Error", msg) + + def start_wifi(self): + wifi_iface = self.wifi_iface_combo.currentText() + ssid = self.ssid_input.text() + password = self.password_input.text() + bridge = self.bridge_select_combo.currentText() + + if not wifi_iface or not ssid or not password or not bridge: + QMessageBox.warning(self, "Error", "Please fill all Wi-Fi fields.") + return + + success, msg = self.nm.setup_wifi_ap(wifi_iface, ssid, password, bridge) + + if success: + # Handle DHCP for Wi-Fi + if self.wifi_enable_dhcp_cb.isChecked(): + ip = self.wifi_bridge_ip.text() + start = self.wifi_dhcp_start.text() + end = self.wifi_dhcp_end.text() + + # Start DHCP on the bridge + d_success, d_msg = self.nm.start_dhcp_server(bridge, ip, start, end) + if d_success: + msg += f"\nDHCP Server running on {ip}" + else: + msg += f"\nDHCP Error: {d_msg}" + + self.wifi_btn.setEnabled(False) + self.stop_wifi_btn.setEnabled(True) + self.client_timer.start() + QMessageBox.information(self, "Success", msg) + else: + QMessageBox.critical(self, "Error", f"Failed to start Wi-Fi AP: {msg}") + + def delete_bridge(self): + bridge = self.active_bridges_combo.currentText() + if not bridge: + return + + confirm = QMessageBox.question(self, "Confirm Deletion", + f"Are you sure you want to delete bridge '{bridge}'?\nThis will stop any associated DHCP servers.", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) + + if confirm == QMessageBox.StandardButton.Yes: + success, msg = self.nm.delete_bridge(bridge) + if success: + QMessageBox.information(self, "Success", msg) + self.refresh_interfaces() + else: + QMessageBox.critical(self, "Error", msg) + + def stop_wifi(self): + iface = self.wifi_iface_combo.currentText() + success, msg = self.nm.stop_wifi_ap(iface) + + if success: + self.wifi_btn.setEnabled(True) + self.stop_wifi_btn.setEnabled(False) + self.client_timer.stop() + self.clients_table.setRowCount(0) + QMessageBox.information(self, "Success", msg) + else: + QMessageBox.critical(self, "Error", msg) + + def refresh_clients(self): + iface = self.wifi_iface_combo.currentText() + clients = self.nm.get_wifi_clients(iface) + + self.clients_table.setRowCount(len(clients)) + for i, client in enumerate(clients): + self.clients_table.setItem(i, 0, QTableWidgetItem(client['mac'])) + self.clients_table.setItem(i, 1, QTableWidgetItem(client['ip'])) + self.clients_table.setItem(i, 2, QTableWidgetItem(client['hostname'])) + + def start_scan(self): + if self.scan_btn.text() == "Stop Scan": + if self.scan_thread: + self.scan_thread.stop() + self.scan_btn.setEnabled(False) + self.scan_btn.setText("Stopping...") + return + + iface = self.scan_iface_combo.currentText() + self.scan_table.setRowCount(0) + self.port_type_label.setText("Port Type: Scanning...") + self.port_type_label.setStyleSheet("font-weight: bold; color: #e0e000;") + + self.scan_btn.setText("Stop Scan") + self.scan_btn.setStyleSheet("background-color: #800000; color: white;") + + self.scan_thread = self.vlan_scanner.scan(iface) + self.scan_thread.vlan_detected.connect(self.on_vlan_detected) + self.scan_thread.untagged_detected.connect(self.on_untagged_detected) + self.scan_thread.scan_finished.connect(self.on_scan_finished) + self.scan_thread.error_occurred.connect(self.on_scan_error) + + def on_untagged_detected(self, count): + self.update_table_row("Untagged (Native)", count) + self.update_port_analysis() + + def on_vlan_detected(self, vlan_id, count): + self.update_table_row(str(vlan_id), count) + self.update_port_analysis() + + def update_table_row(self, label, count): + # Check if row exists + found = False + for row in range(self.scan_table.rowCount()): + if self.scan_table.item(row, 0).text() == label: + self.scan_table.setItem(row, 1, QTableWidgetItem(str(count))) + found = True + break + + if not found: + row = self.scan_table.rowCount() + self.scan_table.insertRow(row) + self.scan_table.setItem(row, 0, QTableWidgetItem(label)) + self.scan_table.setItem(row, 1, QTableWidgetItem(str(count))) + + def update_port_analysis(self): + # Analyze table content to guess port type + has_untagged = False + vlan_count = 0 + + for row in range(self.scan_table.rowCount()): + label = self.scan_table.item(row, 0).text() + if label == "Untagged (Native)": + has_untagged = True + else: + vlan_count += 1 + + if vlan_count > 1: + msg = f"Trunk Port (Detected {vlan_count} VLANs)" + color = "#00e000" # Green + elif vlan_count == 1 and has_untagged: + msg = "Hybrid Port (Tagged + Untagged)" + color = "#e0e000" # Yellow + elif vlan_count == 1: + msg = "Trunk Port (Single VLAN)" + color = "#00e000" + elif has_untagged: + msg = "Access Port (Untagged Traffic Only)" + color = "#00e0e0" # Cyan + else: + msg = "Unknown / No Traffic" + color = "#aaaaaa" + + self.port_type_label.setText(f"Port Type: {msg}") + self.port_type_label.setStyleSheet(f"font-weight: bold; color: {color};") + + def on_scan_finished(self): + self.scan_btn.setEnabled(True) + self.scan_btn.setText("Start Scan (10s)") + self.scan_btn.setStyleSheet("") + # Don't show popup if just stopped, but hard to distinguish here without flag. + # Let's just show it, or maybe check if it was manual stop? + # For now, simple message is fine. + # QMessageBox.information(self, "Scan Finished", "VLAN Scan completed.") + + def on_scan_error(self, msg): + self.scan_btn.setEnabled(True) + self.scan_btn.setText("Start Scan (10s)") + self.scan_btn.setStyleSheet("") + QMessageBox.critical(self, "Error", msg) + + def closeEvent(self, event): + if self.scan_thread and self.scan_thread.isRunning(): + self.scan_thread.stop() + event.accept() diff --git a/src/utils/__pycache__/report_generator.cpython-312.pyc b/src/utils/__pycache__/report_generator.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9f2b044f69bfe15b8bd623fc19719d29167e0146 GIT binary patch literal 5878 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 + + + JNC-NetTools Report + + + +
+

JNC-NetTools Diagnostic Report

+

Generated on: {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}

+ +

Network Interfaces

+ + + """ + + for iface in interfaces: + ip = iface['ipv4'][0] if iface['ipv4'] else "N/A" + status_class = "status-up" if iface['is_up'] else "status-down" + html += f"" + + html += """ +
NameTypeStatusIP AddressMAC Address
{iface['name']}{iface['type']}{iface['status']}{ip}{iface['mac']}
+ +

Port Scan Results

+ + + """ + + if not scan_results: + html += "" + else: + for item in scan_results: + html += f"" + + html += """ +
IPMACVendorStatus
No scan results available.
{item['ip']}{item['mac']}{item['vendor']}{item['status']}
+ +

Anomalies / Failed Traffic (Errors & Retransmissions)

+ + + """ + + if not sniff_failed: + html += "" + else: + for pkt in sniff_failed: + html += f"" + + html += """ +
TimeSourceDestinationProtocolInfoStatus
No anomalies detected.
{pkt['time']}{pkt['src']}{pkt['dst']}{pkt['proto']}{pkt['info']}{pkt['status']}
+ +

Traffic Log (Last 100 Packets)

+ + + """ + + # Limit to last 100 for readability + for pkt in sniff_all[-100:]: + html += f"" + + html += """ +
TimeSourceDestinationProtocolInfoStatus
{pkt['time']}{pkt['src']}{pkt['dst']}{pkt['proto']}{pkt['info']}{pkt['status']}
+
+ + + """ + + try: + with open(filename, "w") as f: + f.write(html) + return True, os.path.abspath(filename) + except Exception as e: + return False, str(e) diff --git a/src/walkthrough.md.resolved b/src/walkthrough.md.resolved new file mode 100644 index 0000000..6396e13 --- /dev/null +++ b/src/walkthrough.md.resolved @@ -0,0 +1,86 @@ +# JNC-Scan Walkthrough + +This guide explains how to run and verify the **JNC-Scan** network diagnostic tool. + +## Prerequisites + +Ensure you have the required system dependencies: +```bash +sudo apt install python3-pyqt6 nmap tcpdump bridge-utils hostapd dnsmasq python3-venv libxcb-cursor0 ethtool +``` + +## Installation (Virtual Environment) + +Since modern Linux distributions manage Python packages externally, we should use a virtual environment: + +1. **Create a virtual environment**: + ```bash + python3 -m venv venv + ``` + +2. **Activate it and install dependencies**: + ```bash + source venv/bin/activate + pip install -r requirements.txt + ``` + +## Running the Application + +**CRITICAL**: The application requires **root privileges**. When using a virtual environment, point `sudo` to the venv's Python executable: + +```bash +sudo ./venv/bin/python3 main.py +``` + +## Verification Steps + +### 1. Interface Management +- **Launch**: Verify the app starts and lists your network interfaces. +- **Details**: Select an interface to see its IP, MAC, and status. + +### 2. IP Configuration & Calculator +- **Static IP**: Go to "IP Configuration". Select an interface, choose "Static", enter an IP (e.g., `192.168.99.1`) and CIDR (`24`). Click "Apply". Verify with `ip addr` in a terminal. +- **Calculator**: Enter an IP and CIDR in the calculator section. Click "Calculate" to see the Netmask and Broadcast address. + +### 3. VLAN & Bridging +- **Wired Bridge**: + 1. Go to "VLAN / Bridge". + 2. Select your main Ethernet (Trunk) and a secondary Ethernet (Access). + 3. Enter a VLAN ID (e.g., `35`). + 4. Click "Create Bridge". + 5. Verify connectivity on the secondary interface. +- **Wi-Fi AP**: + 1. Select a Wireless interface. + 2. Enter SSID and Password. + 3. Select the bridge you just created (e.g., `br35`). + 4. Click "Start Wi-Fi AP". +- **VLAN Scanner**: + 1. In "VLAN / Bridge", look for "Passive VLAN Scanner". + 2. Select an interface. + 3. Click "Start Scan". + 4. Wait 10 seconds. The table will show any VLAN tags detected on the wire. + +### 4. Network Scanner +- Go to "Scanner". +- Enter a range (e.g., `192.168.1.0/24`). +- Click "Scan Network". +- Verify the list of devices appears. + +### 5. Packet Sniffer +- Go to "Sniffer". +- Select an interface (e.g., the bridge `br35`). +- Click "Start Sniffing". +- Generate traffic (ping, browse). +- Verify packets appear in the table. + +### 6. Reporting +- Click "Generate Report" in the toolbar. +- Save the HTML file. +- Open it in a browser to view the summary of interfaces, scan results, and captured packets. + +## Troubleshooting + +- **"No module named..."**: Ensure you are running inside the venv: `sudo ./venv/bin/python3 main.py`. +- **"xcb plugin error"**: Install `libxcb-cursor0`. +- **"Permission denied"**: Ensure you are running with `sudo`. +- **NetworkManager errors**: Ensure `nmcli` is installed (`sudo apt install network-manager`).