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:
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
# 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,27 +991,44 @@ 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:
@ -1004,8 +1037,10 @@ class IPChangerApp:
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,17 +1081,22 @@ 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:")
@ -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
# Guardar IP actual en el historial con la máscara de subred
self.add_to_history(self.ip_prefix.get())
history_prefix = match.group(1).strip()
history_mask = match.group(2).strip()
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("<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():
root = tk.Tk()
app = IPChangerApp(root)
root.mainloop()
if __name__ == "__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"}