import tkinter as tk from tkinter import ttk, messagebox import json import os import subprocess import re import ctypes import sys import ipaddress from typing import List, Optional from dataclasses import dataclass import threading import time try: import wmi WMI_AVAILABLE = True except ImportError: WMI_AVAILABLE = False print("WMI module not found. Using alternative method for network interface detection.") @dataclass class NetworkInterface: name: str description: str ip_address: str = "" subnet_mask: str = "" gateway: str = "" dhcp_enabled: bool = True active: bool = True adapter_id: str = "" class IPChangeConfig: def __init__(self): self.config_file = "ip_config.json" self.history_file = "ip_history.json" self.load_config() def load_config(self): try: if os.path.exists(self.config_file): with open(self.config_file, 'r') as f: config = json.load(f) self.last_interface = config.get('last_interface', '') self.last_ip_prefix = config.get('last_ip_prefix', '') else: self.last_interface = '' self.last_ip_prefix = '' except Exception as e: print(f"Error loading config: {e}") self.last_interface = '' self.last_ip_prefix = '' def save_config(self): try: config = { 'last_interface': self.last_interface, 'last_ip_prefix': self.last_ip_prefix } with open(self.config_file, 'w') as f: json.dump(config, f) except Exception as e: print(f"Error saving config: {e}") class IPChangerApp: def __init__(self, master): self.master = master self.master.title("Network Interface IP Configuration") self.master.geometry("900x700") # Inicializar configuración antes que nada self.config = IPChangeConfig() # Inicializar estructuras de datos self.interfaces: List[NetworkInterface] = [] self.ip_history: List[str] = [] # Inicializar WMI si está disponible if WMI_AVAILABLE: self.wmi_client = wmi.WMI() # Cargar datos guardados antes de crear widgets self.load_ip_history() # Crear la interfaz self.create_widgets() # Actualizar la lista de IPs en el combo self.update_history_display() # Refrescar interfaces self.refresh_interfaces() # Configurar pesos de la cuadrícula self.master.grid_columnconfigure(0, weight=1) self.master.grid_rowconfigure(1, weight=1) # El log expandible def get_ip_prefix(self, ip: str) -> str: """Extrae los primeros 3 octetos de una dirección IP""" parts = ip.split('.') if len(parts) >= 3: return '.'.join(parts[:3]) return ip def create_widgets(self): # Panel superior para controles self.control_frame = ttk.Frame(self.master, padding="10") self.control_frame.grid(row=0, column=0, sticky='new') # Sección de interfaces self.interfaces_frame = ttk.LabelFrame(self.control_frame, text="Network Interfaces", padding="5") self.interfaces_frame.grid(row=0, column=0, sticky='ew', pady=(0,10)) ttk.Label(self.interfaces_frame, text="Select Interface:").grid(row=0, column=0, sticky='w', padx=5) self.if_var = tk.StringVar() self.if_combo = ttk.Combobox(self.interfaces_frame, textvariable=self.if_var, state='readonly', width=50) self.if_combo.grid(row=0, column=1, sticky='ew', padx=5) self.if_combo.bind('<>', self.on_interface_selected) # Info de configuración actual self.current_frame = ttk.LabelFrame(self.control_frame, text="Current Configuration", padding="5") self.current_frame.grid(row=1, column=0, sticky='ew', pady=(0,10)) self.current_ip_var = tk.StringVar() self.current_mask_var = tk.StringVar() self.current_gateway_var = tk.StringVar() self.dhcp_status_var = tk.StringVar() ttk.Label(self.current_frame, text="Current IP:").grid(row=0, column=0, sticky='w', padx=5) ttk.Label(self.current_frame, textvariable=self.current_ip_var).grid(row=0, column=1, sticky='w') ttk.Label(self.current_frame, text="Subnet Mask:").grid(row=1, column=0, sticky='w', padx=5) ttk.Label(self.current_frame, textvariable=self.current_mask_var).grid(row=1, column=1, sticky='w') ttk.Label(self.current_frame, text="Gateway:").grid(row=2, column=0, sticky='w', padx=5) ttk.Label(self.current_frame, textvariable=self.current_gateway_var).grid(row=2, column=1, sticky='w') ttk.Label(self.current_frame, text="DHCP Status:").grid(row=3, column=0, sticky='w', padx=5) ttk.Label(self.current_frame, textvariable=self.dhcp_status_var).grid(row=3, column=1, sticky='w') # Sección de configuración IP self.ip_frame = ttk.LabelFrame(self.control_frame, text="IP Configuration", padding="5") self.ip_frame.grid(row=2, column=0, sticky='ew', pady=(0,10)) ttk.Label(self.ip_frame, text="IP Prefix (first 3 octets):").grid(row=0, column=0, sticky='w', padx=5) self.ip_prefix = tk.StringVar(value=self.config.last_ip_prefix) self.ip_entry = ttk.Entry(self.ip_frame, textvariable=self.ip_prefix, width=30) self.ip_entry.grid(row=0, column=1, sticky='w', padx=5) ttk.Label(self.ip_frame, text="Last Octet:").grid(row=0, column=2, sticky='w', padx=5) self.last_octet = tk.StringVar(value="249") self.last_octet_entry = ttk.Entry(self.ip_frame, textvariable=self.last_octet, width=5) self.last_octet_entry.grid(row=0, column=3, sticky='w', padx=5) # Sección de historial self.history_frame = ttk.LabelFrame(self.control_frame, text="IP History", padding="5") self.history_frame.grid(row=3, column=0, sticky='ew', pady=(0,10)) ttk.Label(self.history_frame, text="Previous IPs:").grid(row=0, column=0, sticky='w', padx=5) self.history_var = tk.StringVar() self.history_combo = ttk.Combobox( self.history_frame, textvariable=self.history_var, state='readonly', width=50 ) self.history_combo.grid(row=0, column=1, sticky='ew', padx=5) self.history_combo.bind('<>', self.on_history_selected) # Botones de acción self.button_frame = ttk.Frame(self.control_frame) self.button_frame.grid(row=4, column=0, sticky='ew', pady=(0,10)) ttk.Button(self.button_frame, text="Set Static IP", command=self.set_static_ip).grid(row=0, column=0, padx=5) ttk.Button(self.button_frame, text="Enable DHCP", command=self.set_dhcp).grid(row=0, column=1, padx=5) ttk.Button(self.button_frame, text="Refresh Interfaces", command=self.refresh_interfaces).grid(row=0, column=2, padx=5) # Log en la parte inferior self.log_frame = ttk.LabelFrame(self.master, text="Operation Log", padding="5") self.log_frame.grid(row=1, column=0, sticky='nsew', padx=10, pady=(0,10)) self.log_text = tk.Text(self.log_frame, wrap="none", height=10) self.log_scrollbar_y = ttk.Scrollbar(self.log_frame, orient="vertical", command=self.log_text.yview) self.log_scrollbar_x = ttk.Scrollbar(self.log_frame, orient="horizontal", command=self.log_text.xview) self.log_text.configure( yscrollcommand=self.log_scrollbar_y.set, xscrollcommand=self.log_scrollbar_x.set ) self.log_text.grid(row=0, column=0, sticky='nsew') self.log_scrollbar_y.grid(row=0, column=1, sticky='ns') self.log_scrollbar_x.grid(row=1, column=0, sticky='ew') ttk.Button(self.log_frame, text="Clear Log", command=self.clear_log).grid(row=2, column=0, columnspan=2, pady=5) self.log_frame.grid_columnconfigure(0, weight=1) self.log_frame.grid_rowconfigure(0, weight=1) def clear_log(self): self.log_text.delete('1.0', tk.END) def log_message(self, message: str): self.log_text.insert(tk.END, f"{message}\n") self.log_text.see(tk.END) self.log_text.update_idletasks() def load_ip_history(self): try: if os.path.exists(self.config.history_file): with open(self.config.history_file, 'r') as f: self.ip_history = json.load(f) else: self.ip_history = [] except Exception as e: self.log_message(f"Error loading IP history: {e}") self.ip_history = [] def save_ip_history(self): try: with open(self.config.history_file, 'w') as f: json.dump(self.ip_history, f) except Exception as e: self.log_message(f"Error saving IP history: {e}") def add_to_history(self, ip_prefix: str): if ip_prefix and ip_prefix not in self.ip_history: self.ip_history.insert(0, ip_prefix) self.ip_history = self.ip_history[:15] # Mantener solo las últimas 15 IPs self.save_ip_history() self.update_history_display() def update_history_display(self): self.history_combo['values'] = self.ip_history if self.ip_history: self.history_combo.set(self.ip_history[0]) def on_history_selected(self, event): selected_ip = self.history_var.get() if selected_ip: # Ensure we only get the first three octets ip_parts = selected_ip.split('.') if len(ip_parts) >= 3: ip_prefix = '.'.join(ip_parts[:3]) self.ip_prefix.set(ip_prefix) self.config.last_ip_prefix = ip_prefix self.config.save_config() self.log_message(f"Selected IP prefix from history: {ip_prefix}") def refresh_interfaces(self): self.interfaces.clear() try: if WMI_AVAILABLE: self._refresh_interfaces_wmi() else: self._refresh_interfaces_netsh() # Actualizar combobox self.if_combo['values'] = [inf.name for inf in self.interfaces if inf.active] # Seleccionar última interfaz usada si está disponible if self.config.last_interface in self.if_combo['values']: self.if_combo.set(self.config.last_interface) self.on_interface_selected() self.log_message("Interfaces refreshed successfully") except Exception as e: self.log_message(f"Error refreshing interfaces: {str(e)}") self.show_error(f"Error refreshing interfaces: {str(e)}") def _refresh_interfaces_wmi(self): try: adapters = self.wmi_client.Win32_NetworkAdapter(PhysicalAdapter=True) configs = self.wmi_client.Win32_NetworkAdapterConfiguration() for adapter in adapters: config = next((cfg for cfg in configs if cfg.InterfaceIndex == adapter.InterfaceIndex), None) if config and config.IPEnabled: interface = NetworkInterface( name=adapter.NetConnectionID, description=adapter.Description, ip_address=config.IPAddress[0] if config.IPAddress else "", subnet_mask=config.IPSubnet[0] if config.IPSubnet else "", gateway=config.DefaultIPGateway[0] if config.DefaultIPGateway else "", dhcp_enabled=config.DHCPEnabled, active=adapter.NetEnabled, adapter_id=adapter.GUID ) self.interfaces.append(interface) except Exception as e: raise Exception(f"WMI refresh error: {str(e)}") def _refresh_interfaces_netsh(self): try: output = subprocess.check_output("netsh interface ipv4 show config", shell=True, text=True) configs = output.split("\n\n") for config in configs: if not config.strip(): continue name_match = re.search(r"Configuración de\s+\"(.+?)\"", config) if not name_match: continue name = name_match.group(1) ip_match = re.search(r"Dirección IP:\s+(\d+\.\d+\.\d+\.\d+)", config) mask_match = re.search(r"Máscara de subred:\s+(\d+\.\d+\.\d+\.\d+)", config) gateway_match = re.search(r"Puerta de enlace predeterminada:\s+(\d+\.\d+\.\d+\.\d+)", config) dhcp_match = re.search(r"DHCP habilitado:\s+(\w+)", config) interface = NetworkInterface( name=name, description=name, ip_address=ip_match.group(1) if ip_match else "", subnet_mask=mask_match.group(1) if mask_match else "", gateway=gateway_match.group(1) if gateway_match else "", dhcp_enabled=dhcp_match.group(1).lower() == "sí" if dhcp_match else True, active=True ) self.interfaces.append(interface) except subprocess.CalledProcessError as e: raise Exception(f"Error executing netsh: {str(e)}") except Exception as e: raise Exception(f"Error parsing netsh output: {str(e)}") def on_interface_selected(self, event=None): selected = self.if_var.get() interface = next((inf for inf in self.interfaces if inf.name == selected), None) if interface: self.current_ip_var.set(interface.ip_address) self.current_mask_var.set(interface.subnet_mask) self.current_gateway_var.set(interface.gateway) self.dhcp_status_var.set("Enabled" if interface.dhcp_enabled else "Disabled") # Actualizar IP prefix si hay una IP válida if interface.ip_address: prefix = '.'.join(interface.ip_address.split('.')[:3]) self.ip_prefix.set(prefix) # Guardar interfaz seleccionada self.config.last_interface = selected self.config.save_config() self.log_message(f"Selected interface: {selected}") def validate_ip_input(self) -> tuple[bool, str]: try: prefix = self.ip_prefix.get().strip() last_octet = self.last_octet.get().strip() # Validar formato del prefijo if 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" # Validar último octeto if not last_octet.isdigit() or not 0 <= int(last_octet) <= 255: return False, "Last octet must be between 0 and 255" # Construir y validar IP completa ip = f"{prefix}.{last_octet}" ipaddress.IPv4Address(ip) return True, ip except ValueError as e: return False, f"Invalid IP address: {str(e)}" def execute_admin_script(self, interface: str, mode: str, debug: bool = True) -> bool: """ Ejecuta el script administrativo con los argumentos proporcionados mode puede ser una IP o 'dhcp' """ # Obtener rutas absolutas current_dir = os.path.dirname(os.path.abspath(__file__)) script_path = os.path.join(current_dir, "ip-changer-admin.py") python_exe = sys.executable # Verificar que el script existe if not os.path.exists(script_path): error_msg = f"Admin script not found at: {script_path}" self.log_message(error_msg) return False # Construir la línea de argumentos args = f'"{script_path}" "{interface}" "{mode}"' if debug: self.log_message("Debug Information:") self.log_message(f"Current Directory: {current_dir}") self.log_message(f"Script Path: {script_path}") self.log_message(f"Python Executable: {python_exe}") self.log_message(f"Full Command: {python_exe} {args}") try: self.log_message(f"Attempting to execute admin script with elevated privileges") ret = ctypes.windll.shell32.ShellExecuteW( None, # hwnd "runas", # operation python_exe, # file args, # parameters current_dir, # directory 1 # show command ) result_code = int(ret) if result_code > 32: self.log_message(f"ShellExecuteW successful (return code: {result_code})") return True else: error_codes = { 0: "The operating system is out of memory or resources", 2: "The specified file was not found", 3: "The specified path was not found", 5: "Access denied", 8: "Insufficient memory to complete the operation", 11: "Bad format", 26: "A sharing violation occurred", 27: "The file name association is incomplete or invalid", 28: "The DDE operation timed out", 29: "The DDE operation failed", 30: "The DDE operation is busy", 31: "The file name association is unavailable", 32: "No application is associated with the file" } self.log_message(f"Failed to execute admin script. Error code {result_code}: {error_msg}") return False except Exception as e: self.log_message(f"Exception while executing admin script: {str(e)}") return False def set_static_ip(self): interface = self.if_var.get() if not interface: self.show_error("Please select a network interface") return # Validar IP is_valid, result = self.validate_ip_input() if not is_valid: self.show_error(result) return ip = result # Guardar IP actual en el historial self.add_to_history(self.ip_prefix.get()) # Ejecutar script con privilegios if self.execute_admin_script(interface, ip, debug=True): time.sleep(2) # Esperar a que se apliquen los cambios self.refresh_interfaces() self.log_message(f"Successfully set static IP {ip} on {interface}") 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: self.show_error("Please select a network interface") return # Ejecutar script con privilegios if self.execute_admin_script(interface, "dhcp", debug=True): time.sleep(2) # Esperar a que se apliquen los cambios self.refresh_interfaces() self.log_message(f"Successfully enabled DHCP on {interface}") else: self.show_error("Failed to enable DHCP. Check the log for details.") def run_command(self, cmd: str) -> bool: self.log_message(f"Executing command: {cmd}") try: process = subprocess.Popen( cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True ) while True: output = process.stdout.readline() if output == '' and process.poll() is not None: break if output: self.log_message(output.strip()) retcode = process.poll() error_output = process.stderr.read() if error_output: self.log_message(f"Error output:\n{error_output}") if retcode == 0: self.log_message("Command executed successfully") return True else: self.log_message(f"Command failed with return code: {retcode}") return False except Exception as e: self.log_message(f"Error executing command: {str(e)}") return False def is_admin(self): try: return ctypes.windll.shell32.IsUserAnAdmin() except Exception as e: self.log_message(f"Error checking admin privileges: {str(e)}") return False def elevate_privileges(self): if sys.platform == 'win32': self.show_info("The application needs administrator privileges to change network settings. " "Please confirm the UAC prompt.") script_path = os.path.abspath(sys.argv[0]) try: ctypes.windll.shell32.ShellExecuteW( None, "runas", sys.executable, f'"{script_path}"', None, 1 ) self.master.quit() except Exception as e: self.log_message(f"Error elevating privileges: {str(e)}") self.show_error("Failed to elevate privileges") def show_error(self, message: str): self.log_message(f"ERROR: {message}") messagebox.showerror("Error", message) def show_info(self, message: str): self.log_message(f"INFO: {message}") messagebox.showinfo("Information", message) def main(): root = tk.Tk() app = IPChangerApp(root) root.mainloop() if __name__ == "__main__": main()