From 5a0a0a140788f754003da05205c7eaaa56a28cc3 Mon Sep 17 00:00:00 2001 From: Miguel Date: Wed, 7 May 2025 16:13:56 +0200 Subject: [PATCH] Updated IP configuration and history; fixed previous IP address and added subnet mask to history entries. Enhanced UI with context menu and subnet mask application functionality. --- ip_config.json | 2 +- ip_history.json | 2 +- menu-ip-change.py | 243 ++++++++++++++++++++++++++++++++++++++-------- ping_targets.json | 2 +- 4 files changed, 206 insertions(+), 43 deletions(-) diff --git a/ip_config.json b/ip_config.json index 679b3a2..4d3bca9 100644 --- a/ip_config.json +++ b/ip_config.json @@ -1 +1 @@ -{"last_interface": "Ethernet", "last_ip_prefix": "10.1.33", "previous_config": {"name": "Ethernet", "ip_address": "192.168.88.249", "subnet_mask": "255.255.255.0", "gateway": "192.168.88.1", "dhcp_enabled": false}, "last_subnet_mask": "255.240.0.0"} \ No newline at end of file +{"last_interface": "Ethernet", "last_ip_prefix": "10.1.33", "previous_config": {"name": "Ethernet", "ip_address": "10.1.33.249", "subnet_mask": "255.255.255.0", "gateway": "10.1.33.1", "dhcp_enabled": false}, "last_subnet_mask": "255.240.0.0"} \ No newline at end of file diff --git a/ip_history.json b/ip_history.json index bb01819..5f5b015 100644 --- a/ip_history.json +++ b/ip_history.json @@ -1 +1 @@ -[{"prefix": "10.1.33", "mask": "255.240.0.0"}, {"prefix": "192.168.88", "mask": "255.255.255.0"}, {"prefix": "192.168.199", "mask": "255.255.255.0"}, {"prefix": "192.168.200", "mask": "255.255.255.0"}, {"prefix": "10.202.4", "mask": "255.255.0.0"}, {"prefix": "169.254.38", "mask": "255.255.0.0"}, {"prefix": "192.168.1", "mask": "255.255.255.0"}, {"prefix": "169.254.69", "mask": "255.255.0.0"}, {"prefix": "192.168.212", "mask": "255.255.255.0"}, {"prefix": "10.1.20", "mask": "255.240.0.0"}, {"prefix": "10.1.22", "mask": "255.255.255.0"}, {"prefix": "10.146.76", "mask": "255.255.255.0"}, {"prefix": "192.168.0", "mask": "255.255.255.0"}, {"prefix": "10.101.8", "mask": "255.255.255.0"}, {"prefix": "10.1.92", "mask": "255.255.255.0"}] \ No newline at end of file +[{"prefix": "10.1.33", "mask": "255.240.0.0"}, {"prefix": "169.254.38", "mask": "255.255.0.0"}, {"prefix": "192.168.1", "mask": "255.255.255.0"}, {"prefix": "192.168.212", "mask": "255.255.255.0"}, {"prefix": "192.168.193", "mask": "255.255.255.0"}, {"prefix": "192.168.88", "mask": "255.255.255.0"}, {"prefix": "192.168.199", "mask": "255.255.255.0"}, {"prefix": "192.168.200", "mask": "255.255.255.0"}, {"prefix": "10.202.4", "mask": "255.255.0.0"}, {"prefix": "169.254.69", "mask": "255.255.0.0"}, {"prefix": "10.1.20", "mask": "255.240.0.0"}, {"prefix": "10.1.22", "mask": "255.255.255.0"}, {"prefix": "10.146.76", "mask": "255.255.255.0"}, {"prefix": "192.168.0", "mask": "255.255.255.0"}, {"prefix": "10.101.8", "mask": "255.255.255.0"}] \ No newline at end of file diff --git a/menu-ip-change.py b/menu-ip-change.py index f9c00e5..4b113fe 100644 --- a/menu-ip-change.py +++ b/menu-ip-change.py @@ -224,6 +224,12 @@ class IPChangerApp: if MAC_LOOKUP_AVAILABLE: self.mac_lookup = MacLookup() + # Crear menú contextual + self.create_context_menu() + + # Manejar el cierre de la ventana + self.master.protocol("WM_DELETE_WINDOW", self.on_app_exit) + def setup_mask_traces(self): """Set up traces for subnet mask and CIDR fields safely""" # First initialize variables @@ -476,6 +482,10 @@ class IPChangerApp: command=lambda: self.set_cidr_preset(24), ).pack(side=tk.LEFT, padx=2) + # Botón para aplicar la máscara de subred + ttk.Button(subnet_frame, text="Apply Mask", command=self.apply_subnet_mask).pack( + side=tk.LEFT, padx=5 + ) # Sección de historial self.history_frame = ttk.LabelFrame( self.control_frame, text="IP History", padding="5" @@ -816,12 +826,17 @@ class IPChangerApp: self.save_ip_history() self.update_history_display() - # Save the last used subnet mask - self.config.last_subnet_mask = subnet_mask - self.config.save_config() + # Ensure the mask_to_use is valid before saving to config + if self.is_valid_subnet_mask(subnet_mask): + self.config.last_ip_prefix = ip_prefix + self.config.last_subnet_mask = subnet_mask + self.config.save_config() + else: + self.log_message(f"Attempted to save invalid mask '{subnet_mask}' to config for prefix '{ip_prefix}'. Skipped.", "WARN") def update_history_display(self): # Create display values for combo box - show prefix and mask + # Ensure ip_history is a list of dicts display_values = [ f"{entry['prefix']} ({entry['mask']})" for entry in self.ip_history ] @@ -851,10 +866,11 @@ class IPChangerApp: if entry["prefix"] == ip_prefix: # Set the subnet mask from history self.subnet_mask.set(entry["mask"]) - # Move this entry to top of history (most recently used) + # Move this entry to top of history and update config self.add_to_history(ip_prefix) break - + # self.config.last_ip_prefix = ip_prefix # Moved to add_to_history + # self.config.last_subnet_mask = self.subnet_mask.get() # Moved to add_to_history self.config.save_config() self.log_message(f"Selected IP prefix from history: {ip_prefix}") @@ -975,37 +991,56 @@ class IPChangerApp: "Enabled" if interface.dhcp_enabled else "Disabled" ) - # Actualizar IP prefix si hay una IP válida + ip_prefix_to_set = "" + subnet_mask_to_set = self.config.last_subnet_mask # Default to last known good mask + source_log_msg = "" + if interface.ip_address: - prefix = ".".join(interface.ip_address.split(".")[:3]) - self.ip_prefix.set(prefix) - - # Set subnet mask from interface + ip_prefix_to_set = ".".join(interface.ip_address.split(".")[:3]) if interface.subnet_mask: - self.subnet_mask.set(interface.subnet_mask) + subnet_mask_to_set = interface.subnet_mask + # If interface.subnet_mask is empty, subnet_mask_to_set remains self.config.last_subnet_mask + source_log_msg = f"from interface {selected}" + elif self.ip_history: # Interface has no IP, try to load from top of history + top_history_entry = self.ip_history[0] + ip_prefix_to_set = top_history_entry["prefix"] + subnet_mask_to_set = top_history_entry["mask"] + source_log_msg = "from IP history (top item)" + else: # No interface IP, no history, use last saved config from startup + ip_prefix_to_set = self.config.last_ip_prefix + subnet_mask_to_set = self.config.last_subnet_mask # Already set as default + source_log_msg = "from last saved configuration" - # Update ping target for the new IP prefix - self.update_ping_target(prefix) + self.ip_prefix.set(ip_prefix_to_set) + self.subnet_mask.set(subnet_mask_to_set) # This will trigger its trace to update CIDR + if ip_prefix_to_set: + self.update_ping_target(ip_prefix_to_set) + # Guardar interfaz seleccionada self.config.last_interface = selected self.config.save_config() - self.log_message(f"Selected interface: {selected}") + self.log_message(f"Selected interface: {selected}. IP fields populated {source_log_msg}.") # Update scan IP range based on current IP and subnet mask + # This uses self.current_ip_var, which is set from interface.ip_address if interface.ip_address and interface.subnet_mask: self.update_scan_ip_range(interface.ip_address, interface.subnet_mask) + # Note: on_subnet_mask_changed (triggered by self.subnet_mask.set) + # will also attempt to update scan range if self.current_ip_var.get() is valid. def validate_ip_input(self) -> tuple[bool, str]: try: prefix = self.ip_prefix.get().strip() last_octet = self.last_octet.get().strip() subnet_mask = self.subnet_mask.get().strip() - + # Validar formato del prefijo - if not re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}", prefix): + if not prefix or not re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}$", prefix): return False, "IP prefix must be in format: xxx.xxx.xxx" + if any(not (0 <= int(p) <= 255) for p in prefix.split(".")): + return False, "Each octet in IP prefix must be between 0 and 255" # Validar último octeto if not last_octet.isdigit() or not 0 <= int(last_octet) <= 255: @@ -1046,18 +1081,23 @@ class IPChangerApp: self.log_message(error_msg) return False - # Construir la línea de argumentos - pass both IP and subnet as a single parameter - if mode != "dhcp": - # For static IP, create the netsh command with subnet mask + # Construir la línea de argumentos para ShellExecuteW + if mode.lower().startswith("netsh"): + # Si 'mode' ya es un comando netsh completo (viene de apply_subnet_mask) + # El script admin lo ejecutará directamente. + args = f'"{script_path}" "{interface}" "{mode}"' + elif mode.lower() == "dhcp": + # Para DHCP, el script admin construirá el comando netsh apropiado. + args = f'"{script_path}" "{interface}" "dhcp"' + else: + # Si 'mode' es una dirección IP (viene de set_static_ip o restore_previous) + # El script admin construirá el comando netsh usando esta IP y la subnet_mask provista. gateway = ( f"{self.get_ip_prefix(mode)}.1" # Use the first three octets of the IP ) - netsh_cmd = f'netsh interface ip set address "{interface}" static {mode} {subnet_mask} {gateway}' - args = f'"{script_path}" "{interface}" "{netsh_cmd}"' - else: - # For DHCP, keep it simple - args = f'"{script_path}" "{interface}" "dhcp"' - + # El script ip-changer-admin.py espera la IP y luego la máscara como argumentos separados si no es un comando netsh completo. + args = f'"{script_path}" "{interface}" "{mode}" "{subnet_mask}"' # Pasamos IP y máscara + if debug: self.log_message("Debug Information:") self.log_message(f"Current Directory: {current_dir}") @@ -1161,13 +1201,19 @@ class IPChangerApp: self.show_error("Previous IP address not available") return - if self.execute_admin_script(interface_name, ip): + # Asegurarse de pasar la máscara de subred correcta al restaurar + prev_mask = prev_config.get("subnet_mask", "255.255.255.0") # Usar máscara guardada o default + if self.execute_admin_script(interface_name, ip, subnet_mask=prev_mask): self.log_message(f"Successfully restored IP {ip} on {interface_name}") time.sleep(2) self.refresh_interfaces() else: self.show_error("Failed to restore static IP configuration") + def get_ip_prefix(self, ip: str) -> str: + """Extrae los primeros 3 octetos de una dirección IP""" + return ".".join(ip.split(".")[:3]) if "." in ip else ip + def do_ping(self): """Realiza un ping a la dirección especificada usando una biblioteca de Python""" target = self.ping_target.get().strip() @@ -1682,41 +1728,81 @@ class IPChangerApp: def set_static_ip(self): interface = self.if_var.get() if not interface: - self.show_error("Please select a network interface") + self.show_error("Please select a network interface.") return # Guardar configuración actual antes de cambiarla self.save_previous_config(interface) - # Validar IP - is_valid, result = self.validate_ip_input() - if not is_valid: - self.show_error(result) + # Obtener IP y máscara del historial seleccionado + selected_history_value = self.history_var.get() + if not selected_history_value: + self.show_error("Please select an IP configuration from the 'Previous IPs' list or ensure the list is not empty.") return - ip = result - subnet_mask = self.subnet_mask.get() + try: + # Parsear: "192.168.1 (255.255.255.0)" + match = re.match(r"^(.*?) \((.*?)\)$", selected_history_value) + if not match: + self.show_error(f"Invalid format in 'Previous IPs' selection: {selected_history_value}") + return + + history_prefix = match.group(1).strip() + history_mask = match.group(2).strip() - # Guardar IP actual en el historial con la máscara de subred - self.add_to_history(self.ip_prefix.get()) + if not self.is_valid_subnet_mask(history_mask): + self.show_error(f"Invalid subnet mask '{history_mask}' from history selection.") + return + + if not re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}$", history_prefix) or \ + any(not (0 <= int(p) <= 255) for p in history_prefix.split(".")): + self.show_error(f"Invalid IP prefix '{history_prefix}' from history selection.") + return + + except Exception as e: + self.show_error(f"Error parsing history selection: {str(e)}") + return + + # Obtener el último octeto del campo de entrada + last_octet_str = self.last_octet.get().strip() + if not last_octet_str.isdigit() or not (0 <= int(last_octet_str) <= 255): + self.show_error("Last octet must be a number between 0 and 255.") + return + + full_ip = f"{history_prefix}.{last_octet_str}" + + try: + ipaddress.IPv4Address(full_ip) # Validar IP completa + except ValueError: + self.show_error(f"Constructed IP address '{full_ip}' is invalid.") + return + + # Poblar los campos de la UI para consistencia y para add_to_history + self.ip_prefix.set(history_prefix) + self.subnet_mask.set(history_mask) # Esto también actualizará el CIDR + + # Guardar IP (prefijo y máscara) en el historial. + # add_to_history usará self.ip_prefix.get() y self.subnet_mask.get() + self.add_to_history(history_prefix) # Log the actual command we'll be executing - gateway = f"{self.ip_prefix.get()}.1" - command = f'netsh interface ip set address "{interface}" static {ip} {subnet_mask} {gateway}' + gateway = f"{history_prefix}.1" # Gateway por defecto + command = f'netsh interface ip set address "{interface}" static {full_ip} {history_mask} {gateway}' self.log_message(f"Executing network command: {command}") # Ejecutar script con privilegios - pass subnet mask if self.execute_admin_script( - interface, ip, debug=True, subnet_mask=subnet_mask + interface, full_ip, debug=True, subnet_mask=history_mask ): time.sleep(2) # Esperar a que se apliquen los cambios self.refresh_interfaces() self.log_message( - f"Successfully set static IP {ip} with mask {subnet_mask} on {interface}" + f"Successfully set static IP {full_ip} with mask {history_mask} on {interface} using history." ) else: self.show_error("Failed to set static IP. Check the log for details.") + def set_dhcp(self): interface = self.if_var.get() if not interface: @@ -2125,12 +2211,89 @@ class IPChangerApp: self.cidr_bits.set(str(cidr_bits)) # The on_cidr_bits_changed trace will update the subnet mask automatically + def apply_subnet_mask(self): + """ + Explicitly applies the current subnet mask, triggering updates for CIDR + and applying the mask to the selected interface, keeping its current IP and gateway. + """ + if_name = self.if_var.get() + if not if_name: + self.show_error("Please select a network interface.") + return + + current_ip = self.current_ip_var.get() + if not current_ip: + self.show_error("Selected interface has no current IP address to apply mask to.") + return + try: + ipaddress.IPv4Address(current_ip) # Validate current IP + except ValueError: + self.show_error(f"Current IP address '{current_ip}' of the interface is invalid.") + return + + new_mask = self.subnet_mask.get().strip() + if not self.is_valid_subnet_mask(new_mask): + self.show_error(f"The entered subnet mask '{new_mask}' is invalid.") + return + + current_gateway = self.current_gateway_var.get().strip() + if not current_gateway: # If no gateway, calculate a default one + current_ip_prefix = self.get_ip_prefix(current_ip) + gateway_to_use = f"{current_ip_prefix}.1" + self.log_message(f"No current gateway found, using default: {gateway_to_use}", "WARN") + else: + gateway_to_use = current_gateway + + self.save_previous_config(if_name) + + # Construir el comando netsh completo + netsh_cmd = f'netsh interface ip set address "{if_name}" static {current_ip} {new_mask} {gateway_to_use}' + self.log_message(f"Preparing to apply new mask. Command: {netsh_cmd}") + + # execute_admin_script tomará netsh_cmd como 'mode' y lo pasará al script admin + if self.execute_admin_script(if_name, mode=netsh_cmd, debug=True, subnet_mask=new_mask): # subnet_mask aquí es solo para logueo en debug + time.sleep(2) + # self.subnet_mask.set(new_mask) ya debería estar hecho por el usuario + self.add_to_history(self.get_ip_prefix(current_ip)) # Esto usará new_mask de self.subnet_mask + self.refresh_interfaces() + self.log_message(f"Successfully applied subnet mask {new_mask} to {current_ip} on {if_name}.") + else: + self.show_error(f"Failed to apply subnet mask to {if_name}. Check log.") + + + def create_context_menu(self): + """Crea el menú contextual para la ventana principal.""" + self.context_menu = tk.Menu(self.master, tearoff=0) + self.context_menu.add_command( + label="Open Project Folder", command=self.open_project_folder + ) + + self.master.bind("", self.show_context_menu) # Button-3 es clic derecho + + def show_context_menu(self, event): + """Muestra el menú contextual en la posición del cursor.""" + try: + self.context_menu.tk_popup(event.x_root, event.y_root) + finally: + self.context_menu.grab_release() + + def open_project_folder(self): + """Abre la carpeta del proyecto en el explorador de archivos.""" + try: + script_dir = os.path.dirname(os.path.abspath(sys.argv[0])) + subprocess.run(['explorer', script_dir]) # Quitado check=True + self.log_message(f"Opened project folder: {script_dir}") + except Exception as e: + self.log_message(f"Error opening project folder: {e}", "ERROR") + + def on_app_exit(self): + """Maneja el cierre de la aplicación, incluyendo la restauración del WindowProc.""" + self.master.destroy() def main(): root = tk.Tk() app = IPChangerApp(root) root.mainloop() - if __name__ == "__main__": main() diff --git a/ping_targets.json b/ping_targets.json index 172fdc2..4e19872 100644 --- a/ping_targets.json +++ b/ping_targets.json @@ -1 +1 @@ -{"10.1.22": "10.1.22.11", "10.138.182": "10.138.182.94", "10.1.20": "10.1.33.11", "10.255.255": "10.255.255.1", "192.168.88": "192.168.88.1", "192.168.212": "192.168.212.212", "192.168.1": "192.168.1.212", "10.1.33": "192.168.88.1", "192.168.193": "192.168.1.212"} \ No newline at end of file +{"10.1.22": "10.1.22.11", "10.138.182": "10.138.182.94", "10.1.20": "10.1.33.11", "10.255.255": "10.255.255.1", "192.168.88": "192.168.88.1", "192.168.212": "192.168.212.212", "192.168.1": "192.168.1.212", "10.1.33": "192.168.88.1", "192.168.193": "192.168.1.212", "172.25.77": "172.25.77.1"} \ No newline at end of file