""" Widget de detección de VLANs en un trunk (Scapy). Funciona en Windows (con Npcap) y en Linux. """ from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QComboBox, QPushButton, QGroupBox, QTableWidget, QTableWidgetItem, QHeaderView, QMessageBox, ) from PyQt6.QtCore import Qt from src.core.vlan_scanner import VLANScanner class VLANDetectionWidget(QWidget): """Solo detección pasiva de VLANs (Scapy). Sin bridge ni tcpdump.""" def __init__(self, network_manager): super().__init__() self.nm = network_manager self.vlan_scanner = VLANScanner() self.scan_thread = None layout = QVBoxLayout(self) group = QGroupBox("Detección de VLANs en trunk") group_layout = QVBoxLayout(group) controls = QHBoxLayout() controls.addWidget(QLabel("Interfaz:")) self.iface_combo = QComboBox() controls.addWidget(self.iface_combo) self.scan_btn = QPushButton("Iniciar escaneo (10 s)") self.scan_btn.clicked.connect(self.start_scan) controls.addWidget(self.scan_btn) group_layout.addLayout(controls) self.scan_table = QTableWidget() self.scan_table.setColumnCount(2) self.scan_table.setHorizontalHeaderLabels(["VLAN ID", "Paquetes detectados"]) self.scan_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) self.scan_table.setMinimumHeight(200) group_layout.addWidget(self.scan_table) self.port_type_label = QLabel("Tipo de puerto: —") self.port_type_label.setStyleSheet("font-weight: bold; color: #aaaaaa;") group_layout.addWidget(self.port_type_label) layout.addWidget(group) info = QLabel( "Conecta esta interfaz a un puerto trunk y pulsa Iniciar escaneo. " "En Windows requiere Npcap y ejecutar como Administrador." ) info.setWordWrap(True) info.setStyleSheet("color: #888; font-size: 11px;") layout.addWidget(info) self.refresh_interfaces() def refresh_interfaces(self): self.iface_combo.clear() for iface in self.nm.list_interfaces(): self.iface_combo.addItem(iface["name"]) def start_scan(self): if self.scan_thread and self.scan_thread.isRunning(): self.scan_thread.stop() self.scan_btn.setEnabled(False) self.scan_btn.setText("Deteniendo...") return iface = self.iface_combo.currentText() if not iface: QMessageBox.warning(self, "Error", "Selecciona una interfaz.") return self.scan_table.setRowCount(0) self.port_type_label.setText("Tipo de puerto: Escaneando...") self.port_type_label.setStyleSheet("font-weight: bold; color: #e0e000;") self.scan_btn.setText("Detener escaneo") self.scan_btn.setStyleSheet("background-color: #800000; color: white;") self.scan_thread = self.vlan_scanner.scan_scapy(iface, timeout=10) 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_row("Untagged (Native)", count) self._update_port_analysis() def on_vlan_detected(self, vlan_id, count): self._update_row(str(vlan_id), count) self._update_port_analysis() def _update_row(self, label, count): 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))) return 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): 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 ({vlan_count} VLANs)" color = "#00e000" elif vlan_count == 1 and has_untagged: msg = "Híbrido (tagged + untagged)" color = "#e0e000" elif vlan_count == 1: msg = "Trunk (1 VLAN)" color = "#00e000" elif has_untagged: msg = "Access (solo untagged)" color = "#00e0e0" else: msg = "— / Sin tráfico" color = "#aaaaaa" self.port_type_label.setText(f"Tipo de puerto: {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("Iniciar escaneo (10 s)") self.scan_btn.setStyleSheet("") def on_scan_error(self, msg): self.scan_btn.setEnabled(True) self.scan_btn.setText("Iniciar escaneo (10 s)") 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()