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.

This commit is contained in:
Miguel 2025-05-07 16:13:56 +02:00
parent 5fea463343
commit 5a0a0a1407
4 changed files with 206 additions and 43 deletions

View File

@ -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"} {"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"}

View File

@ -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"}] [{"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"}]

View File

@ -224,6 +224,12 @@ class IPChangerApp:
if MAC_LOOKUP_AVAILABLE: if MAC_LOOKUP_AVAILABLE:
self.mac_lookup = MacLookup() 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): def setup_mask_traces(self):
"""Set up traces for subnet mask and CIDR fields safely""" """Set up traces for subnet mask and CIDR fields safely"""
# First initialize variables # First initialize variables
@ -476,6 +482,10 @@ class IPChangerApp:
command=lambda: self.set_cidr_preset(24), command=lambda: self.set_cidr_preset(24),
).pack(side=tk.LEFT, padx=2) ).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 # Sección de historial
self.history_frame = ttk.LabelFrame( self.history_frame = ttk.LabelFrame(
self.control_frame, text="IP History", padding="5" self.control_frame, text="IP History", padding="5"
@ -816,12 +826,17 @@ class IPChangerApp:
self.save_ip_history() self.save_ip_history()
self.update_history_display() self.update_history_display()
# Save the last used subnet mask # Ensure the mask_to_use is valid before saving to config
self.config.last_subnet_mask = subnet_mask if self.is_valid_subnet_mask(subnet_mask):
self.config.save_config() 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): def update_history_display(self):
# Create display values for combo box - show prefix and mask # Create display values for combo box - show prefix and mask
# Ensure ip_history is a list of dicts
display_values = [ display_values = [
f"{entry['prefix']} ({entry['mask']})" for entry in self.ip_history f"{entry['prefix']} ({entry['mask']})" for entry in self.ip_history
] ]
@ -851,10 +866,11 @@ class IPChangerApp:
if entry["prefix"] == ip_prefix: if entry["prefix"] == ip_prefix:
# Set the subnet mask from history # Set the subnet mask from history
self.subnet_mask.set(entry["mask"]) 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) self.add_to_history(ip_prefix)
break 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.config.save_config()
self.log_message(f"Selected IP prefix from history: {ip_prefix}") self.log_message(f"Selected IP prefix from history: {ip_prefix}")
@ -975,37 +991,56 @@ class IPChangerApp:
"Enabled" if interface.dhcp_enabled else "Disabled" "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: if interface.ip_address:
prefix = ".".join(interface.ip_address.split(".")[:3]) ip_prefix_to_set = ".".join(interface.ip_address.split(".")[:3])
self.ip_prefix.set(prefix)
# Set subnet mask from interface
if interface.subnet_mask: 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.ip_prefix.set(ip_prefix_to_set)
self.update_ping_target(prefix) 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 # Guardar interfaz seleccionada
self.config.last_interface = selected self.config.last_interface = selected
self.config.save_config() 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 # 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: if interface.ip_address and interface.subnet_mask:
self.update_scan_ip_range(interface.ip_address, 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]: def validate_ip_input(self) -> tuple[bool, str]:
try: try:
prefix = self.ip_prefix.get().strip() prefix = self.ip_prefix.get().strip()
last_octet = self.last_octet.get().strip() last_octet = self.last_octet.get().strip()
subnet_mask = self.subnet_mask.get().strip() subnet_mask = self.subnet_mask.get().strip()
# Validar formato del prefijo # 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" 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 # Validar último octeto
if not last_octet.isdigit() or not 0 <= int(last_octet) <= 255: if not last_octet.isdigit() or not 0 <= int(last_octet) <= 255:
@ -1046,18 +1081,23 @@ class IPChangerApp:
self.log_message(error_msg) self.log_message(error_msg)
return False return False
# Construir la línea de argumentos - pass both IP and subnet as a single parameter # Construir la línea de argumentos para ShellExecuteW
if mode != "dhcp": if mode.lower().startswith("netsh"):
# For static IP, create the netsh command with subnet mask # 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 = ( gateway = (
f"{self.get_ip_prefix(mode)}.1" # Use the first three octets of the IP 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}' # 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}" "{netsh_cmd}"' args = f'"{script_path}" "{interface}" "{mode}" "{subnet_mask}"' # Pasamos IP y máscara
else:
# For DHCP, keep it simple
args = f'"{script_path}" "{interface}" "dhcp"'
if debug: if debug:
self.log_message("Debug Information:") self.log_message("Debug Information:")
self.log_message(f"Current Directory: {current_dir}") self.log_message(f"Current Directory: {current_dir}")
@ -1161,13 +1201,19 @@ class IPChangerApp:
self.show_error("Previous IP address not available") self.show_error("Previous IP address not available")
return 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}") self.log_message(f"Successfully restored IP {ip} on {interface_name}")
time.sleep(2) time.sleep(2)
self.refresh_interfaces() self.refresh_interfaces()
else: else:
self.show_error("Failed to restore static IP configuration") 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): def do_ping(self):
"""Realiza un ping a la dirección especificada usando una biblioteca de Python""" """Realiza un ping a la dirección especificada usando una biblioteca de Python"""
target = self.ping_target.get().strip() target = self.ping_target.get().strip()
@ -1682,41 +1728,81 @@ class IPChangerApp:
def set_static_ip(self): def set_static_ip(self):
interface = self.if_var.get() interface = self.if_var.get()
if not interface: if not interface:
self.show_error("Please select a network interface") self.show_error("Please select a network interface.")
return return
# Guardar configuración actual antes de cambiarla # Guardar configuración actual antes de cambiarla
self.save_previous_config(interface) self.save_previous_config(interface)
# Validar IP # Obtener IP y máscara del historial seleccionado
is_valid, result = self.validate_ip_input() selected_history_value = self.history_var.get()
if not is_valid: if not selected_history_value:
self.show_error(result) self.show_error("Please select an IP configuration from the 'Previous IPs' list or ensure the list is not empty.")
return return
ip = result try:
subnet_mask = self.subnet_mask.get() # 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 if not self.is_valid_subnet_mask(history_mask):
self.add_to_history(self.ip_prefix.get()) 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 # Log the actual command we'll be executing
gateway = f"{self.ip_prefix.get()}.1" gateway = f"{history_prefix}.1" # Gateway por defecto
command = f'netsh interface ip set address "{interface}" static {ip} {subnet_mask} {gateway}' command = f'netsh interface ip set address "{interface}" static {full_ip} {history_mask} {gateway}'
self.log_message(f"Executing network command: {command}") self.log_message(f"Executing network command: {command}")
# Ejecutar script con privilegios - pass subnet mask # Ejecutar script con privilegios - pass subnet mask
if self.execute_admin_script( 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 time.sleep(2) # Esperar a que se apliquen los cambios
self.refresh_interfaces() self.refresh_interfaces()
self.log_message( 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: else:
self.show_error("Failed to set static IP. Check the log for details.") self.show_error("Failed to set static IP. Check the log for details.")
def set_dhcp(self): def set_dhcp(self):
interface = self.if_var.get() interface = self.if_var.get()
if not interface: if not interface:
@ -2125,12 +2211,89 @@ class IPChangerApp:
self.cidr_bits.set(str(cidr_bits)) self.cidr_bits.set(str(cidr_bits))
# The on_cidr_bits_changed trace will update the subnet mask automatically # 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("<Button-3>", 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(): def main():
root = tk.Tk() root = tk.Tk()
app = IPChangerApp(root) app = IPChangerApp(root)
root.mainloop() root.mainloop()
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@ -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"} {"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"}