""" Clase híbrida para direcciones IPv4 """ from class_base import ClassBase from sympy_Base import SympyClassBase from typing import Optional, Union, List, Tuple import re import sympy class IP4Mask(ClassBase): """ Helper class to manage IPv4 masks. It can be initialized with an integer prefix (0-32) or a netmask string (e.g., "255.255.255.0"). """ _prefix: int _mask_int: int def __init__(self, mask_input: Union[int, str]): prefix = self._parse_mask(mask_input) super().__init__(prefix, str(mask_input)) self._prefix = prefix self._mask_int = self._prefix_to_mask_int(self._prefix) def _parse_mask(self, mask_input: Union[int, str]) -> int: """Helper to parse mask_input and return prefix.""" prefix_val: int if isinstance(mask_input, int): if not (0 <= mask_input <= 32): raise ValueError(f"Invalid prefix length: {mask_input}. Must be between 0 and 32.") prefix_val = mask_input elif isinstance(mask_input, str): try: parsed_int = int(mask_input) if not (0 <= parsed_int <= 32): raise ValueError(f"Invalid prefix string: '{mask_input}'. Must be between 0 and 32.") prefix_val = parsed_int except ValueError: parsed_prefix_from_str = self._netmask_str_to_prefix(mask_input) if parsed_prefix_from_str is None: raise ValueError(f"Invalid netmask string format: '{mask_input}'.") prefix_val = parsed_prefix_from_str else: raise TypeError(f"Invalid type for mask_input: {type(mask_input)}. Must be int or str.") return prefix_val @staticmethod def _is_valid_ip_octet_str(octet_str: str) -> bool: try: octet_val = int(octet_str) return 0 <= octet_val <= 255 except ValueError: return False @staticmethod def _is_valid_ip_string(ip_str: str) -> bool: parts = ip_str.split('.') if len(parts) != 4: return False return all(IP4Mask._is_valid_ip_octet_str(p.strip()) for p in parts) @staticmethod def _netmask_str_to_prefix(netmask_str: str) -> Optional[int]: if not IP4Mask._is_valid_ip_string(netmask_str): return None octets = [int(x) for x in netmask_str.split('.')] mask_val = (octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3] binary_mask = bin(mask_val)[2:].zfill(32) if not re.fullmatch(r"1*0*", binary_mask): # Must be contiguous 1s followed by 0s return None return binary_mask.count('1') @staticmethod def _prefix_to_mask_int(prefix: int) -> int: if not (0 <= prefix <= 32): # Should be validated before calling raise ValueError("Prefix out of range") if prefix == 0: return 0 return (0xFFFFFFFF << (32 - prefix)) & 0xFFFFFFFF def get_mask_str(self) -> str: return f"{(self._mask_int >> 24) & 0xFF}.{(self._mask_int >> 16) & 0xFF}.{(self._mask_int >> 8) & 0xFF}.{self._mask_int & 0xFF}" def get_prefix_int(self) -> int: return self._prefix def __str__(self) -> str: return self.get_mask_str() def __repr__(self) -> str: return f"IP4Mask({self._prefix})" def __eq__(self, other): if isinstance(other, IP4Mask): return self._prefix == other._prefix return False @staticmethod def Helper(input_str): if re.match(r"^\s*IP4Mask\b", input_str, re.IGNORECASE): return 'Máscara: IP4Mask[24], IP4Mask[255.255.255.0]. Funciones: get_prefix_int(), get_mask_str(), hosts_count(), to_hex(), to_binary()' return None @staticmethod def PopupFunctionList(): return [ ("get_prefix_int", "Obtiene prefijo CIDR (ej: 24)"), ("get_mask_str", "Obtiene máscara como string (ej: 255.255.255.0)"), ("to_dotted", "Alias para get_mask_str()"), ("to_prefix", "Alias para get_prefix_int()"), ("hosts_count", "Número de hosts disponibles"), ("total_addresses", "Total de direcciones (incluyendo red/broadcast)"), ("to_decimal", "Convierte a entero decimal"), ("to_hex", "Convierte a hexadecimal"), ("to_binary", "Representación binaria"), ("to_wildcard", "Máscara wildcard (inversa)"), ("bit_representation", "Representación visual con bits"), ("octet_breakdown", "Desglose por octetos"), ("is_more_specific", "¿Es más específica que otra máscara?"), ("is_subnet_of", "¿Es subred de otra máscara?"), ("common_supernet", "Máscara común que contiene ambas"), ("next_smaller", "Siguiente máscara más pequeña"), ("next_larger", "Siguiente máscara más grande"), ("split", "Dividir en dos subredes"), ("is_valid", "¿Es una máscara válida?"), ("is_host_mask", "¿Es máscara de host (/32)?"), ("is_network_mask", "¿Es máscara de red (/0)?"), ("efficiency", "Eficiencia de uso de direcciones"), ("to_sympy", "Convierte a expresión SymPy para álgebra"), ] # ========== CONVERSIONES Y REPRESENTACIONES ========== def to_decimal(self) -> int: """Convierte a entero decimal""" return self._mask_int def to_hex(self) -> str: """Convierte a hexadecimal""" return f"0x{self._mask_int:08X}" def to_binary(self) -> str: """Representación binaria de 32 bits""" return f"{self._mask_int:032b}" def to_dotted(self) -> str: """Alias para get_mask_str()""" return self.get_mask_str() def to_prefix(self) -> int: """Alias para get_prefix_int()""" return self.get_prefix_int() def to_wildcard(self) -> str: """Máscara wildcard (inversa)""" wildcard_int = self._mask_int ^ 0xFFFFFFFF return f"{(wildcard_int >> 24) & 0xFF}.{(wildcard_int >> 16) & 0xFF}.{(wildcard_int >> 8) & 0xFF}.{wildcard_int & 0xFF}" def bit_representation(self) -> str: """Representación visual con bits""" binary = self.to_binary() return f"{binary[:8]}.{binary[8:16]}.{binary[16:24]}.{binary[24:32]}".replace('1', '█').replace('0', '.') def octet_breakdown(self) -> dict: """Desglose por octetos""" return { "oct1": (self._mask_int >> 24) & 0xFF, "oct2": (self._mask_int >> 16) & 0xFF, "oct3": (self._mask_int >> 8) & 0xFF, "oct4": self._mask_int & 0xFF } # ========== CÁLCULOS DE RED ========== def hosts_count(self) -> int: """Número de hosts disponibles (excluyendo red/broadcast)""" if self._prefix >= 31: return 0 return (2 ** (32 - self._prefix)) - 2 def total_addresses(self) -> int: """Total de direcciones (incluyendo red/broadcast)""" return 2 ** (32 - self._prefix) def subnet_count(self, base_mask: 'IP4Mask') -> int: """¿Cuántas subredes de este tamaño caben en la máscara base?""" if not isinstance(base_mask, IP4Mask): raise TypeError("base_mask must be IP4Mask instance") if self._prefix < base_mask._prefix: raise ValueError("Target mask must be more specific than base mask") return 2 ** (self._prefix - base_mask._prefix) def efficiency(self) -> float: """Eficiencia de uso de direcciones (0-1)""" if self._prefix >= 31: return 1.0 if self._prefix == 32 else 0.0 return self.hosts_count() / self.total_addresses() # ========== COMPARACIONES Y ANÁLISIS ========== def is_more_specific(self, other_mask: 'IP4Mask') -> bool: """¿Es más específica (mayor prefijo) que otra máscara?""" if not isinstance(other_mask, IP4Mask): raise TypeError("other_mask must be IP4Mask instance") return self._prefix > other_mask._prefix def is_subnet_of(self, other_mask: 'IP4Mask') -> bool: """¿Es subred (más específica) de otra máscara?""" return self.is_more_specific(other_mask) def common_supernet(self, other_mask: 'IP4Mask') -> 'IP4Mask': """Máscara común que contiene ambas""" if not isinstance(other_mask, IP4Mask): raise TypeError("other_mask must be IP4Mask instance") common_prefix = min(self._prefix, other_mask._prefix) return IP4Mask(common_prefix) # ========== UTILIDADES DE PROGRAMADOR ========== def is_valid(self) -> bool: """¿Es una máscara válida?""" return 0 <= self._prefix <= 32 def is_host_mask(self) -> bool: """¿Es máscara de host (/32)?""" return self._prefix == 32 def is_network_mask(self) -> bool: """¿Es máscara de red (/0)?""" return self._prefix == 0 def next_smaller(self) -> 'IP4Mask': """Siguiente máscara más pequeña (más específica)""" if self._prefix >= 32: raise ValueError("Cannot make /32 more specific") return IP4Mask(self._prefix + 1) def next_larger(self) -> 'IP4Mask': """Siguiente máscara más grande (menos específica)""" if self._prefix <= 0: raise ValueError("Cannot make /0 less specific") return IP4Mask(self._prefix - 1) def split(self) -> Tuple['IP4Mask', 'IP4Mask']: """Divide en dos subredes del siguiente tamaño""" if self._prefix >= 32: raise ValueError("Cannot split /32 mask") smaller_mask = self.next_smaller() return (smaller_mask, smaller_mask) def to_sympy(self): """Convierte a SymPy cuando se necesite álgebra""" return sympy.sympify(self._prefix) def hosts_expression(self): """Expresión SymPy para número de hosts""" return 2**(32 - self._prefix) # ========== MÉTODOS ESTÁTICOS ========== @staticmethod def optimal_for_hosts(host_count: int) -> 'IP4Mask': """¿Qué máscara para N hosts?""" if host_count <= 0: return IP4Mask(32) if host_count == 1: return IP4Mask(31) # Encontrar el menor prefijo que acomode host_count + 2 (red y broadcast) total_needed = host_count + 2 prefix = 32 while 2**(32 - prefix) < total_needed and prefix > 0: prefix -= 1 return IP4Mask(prefix) @staticmethod def compare_efficiency(mask_list: List['IP4Mask']) -> List[Tuple['IP4Mask', float]]: """Eficiencia de uso de cada máscara""" return [(mask, mask.efficiency()) for mask in mask_list] class Class_IP4(SympyClassBase): """Clase híbrida para direcciones IPv4""" def __new__(cls, *args, **kwargs): temp_ip_str_candidate = args[0] if args else "" temp_ip_str = "" if isinstance(temp_ip_str_candidate, str): if '/' in temp_ip_str_candidate: temp_ip_str = temp_ip_str_candidate.split('/', 1)[0].strip() elif ' ' in temp_ip_str_candidate: temp_ip_str = temp_ip_str_candidate.split(' ', 1)[0].strip() else: temp_ip_str = temp_ip_str_candidate.strip() if IP4Mask._is_valid_ip_string(temp_ip_str): ip_parts_for_new = [int(x) for x in temp_ip_str.split('.')] ip_int_for_new = (ip_parts_for_new[0] << 24) | \ (ip_parts_for_new[1] << 16) | \ (ip_parts_for_new[2] << 8) | \ ip_parts_for_new[3] obj = SympyClassBase.__new__(cls, ip_int_for_new) else: raise ValueError(f"Invalid IP address format for __new__: {temp_ip_str}") else: if not args: raise ValueError("IP4 constructor requires arguments.") obj = SympyClassBase.__new__(cls, *args) return obj def __init__(self, *args): self._raw_constructor_args = args ip_str: str self._mask_obj: Optional[IP4Mask] = None if not args: raise ValueError("IP4 constructor requires at least one argument (the IP string).") ip_str_candidate = args[0] if not isinstance(ip_str_candidate, str): raise TypeError(f"First argument to IP4 must be a string (IP address), got {type(ip_str_candidate)}") if len(args) == 1: input_str = args[0] if '/' in input_str: parts = input_str.split('/', 1) ip_str = parts[0].strip() if len(parts) > 1 and parts[1].strip(): self._mask_obj = IP4Mask(parts[1].strip()) elif ' ' in input_str: parts = input_str.split(' ', 1) ip_str = parts[0].strip() if len(parts) > 1 and parts[1].strip(): self._mask_obj = IP4Mask(parts[1].strip()) else: ip_str = input_str.strip() elif len(args) == 2: ip_str = args[0].strip() mask_arg = args[1] if isinstance(mask_arg, IP4Mask): self._mask_obj = mask_arg elif isinstance(mask_arg, (str, int)): self._mask_obj = IP4Mask(mask_arg) else: raise TypeError(f"Second argument (mask) for IP4 must be int, str, or IP4Mask instance, got {type(mask_arg)}") else: raise ValueError(f"IP4 constructor takes 1 or 2 arguments, got {len(args)}: {args}") if not IP4Mask._is_valid_ip_string(ip_str): raise ValueError(f"Invalid IP address format: {ip_str}") self._ip_str = ip_str ip_parts = [int(x) for x in ip_str.split('.')] self._ip_int = (ip_parts[0] << 24) | (ip_parts[1] << 16) | (ip_parts[2] << 8) | ip_parts[3] sympy_base_original_str: str if len(args) == 1 and isinstance(args[0], str): sympy_base_original_str = args[0] elif len(args) == 2: mask_arg_for_repr = args[1] if isinstance(mask_arg_for_repr, IP4Mask): mask_repr_str = str(mask_arg_for_repr.get_prefix_int()) elif isinstance(mask_arg_for_repr, int): mask_repr_str = str(mask_arg_for_repr) else: mask_repr_str = str(mask_arg_for_repr) sympy_base_original_str = f"{args[0]};{mask_repr_str}" else: sympy_base_original_str = self._ip_str super().__init__(self._ip_int, sympy_base_original_str) def __repr__(self): arg_reprs = [repr(arg) for arg in self._raw_constructor_args] return f"{self.__class__.__name__}({', '.join(arg_reprs)})" def __str__(self): if self._mask_obj: return f"{self._ip_str}/{self._mask_obj.get_prefix_int()}" return self._ip_str def _sympystr(self, printer): return str(self) @staticmethod def Helper(input_str): if re.match(r"^\s*IP4\b", input_str, re.IGNORECASE): return ('Ej: IP4[192.168.1.1/24], IP4[10.0.0.1;8]. Funciones: NetworkAddress(), BroadcastAddress(), add_hosts(), next_ip(), mask()') return None @staticmethod def PopupFunctionList(): return [ ("NetworkAddress", "Dirección de red"), ("BroadcastAddress", "Dirección de broadcast"), ("Nodes", "Hosts disponibles"), ("mask", "Objeto máscara con sus métodos"), ("add_hosts", "Sumar N hosts a la dirección"), ("subtract_hosts", "Restar N hosts a la dirección"), ("next_ip", "Siguiente dirección IP"), ("previous_ip", "Dirección IP anterior"), ("first_host", "Primera IP asignable"), ("last_host", "Última IP asignable"), ("is_network_address", "¿Es dirección de red?"), ("is_broadcast_address", "¿Es dirección de broadcast?"), ("is_host_address", "¿Es dirección de host válida?"), ("is_first_host", "¿Es la primera IP asignable?"), ("is_last_host", "¿Es la última IP asignable?"), ("to_integer", "Convierte a entero"), ("to_hex", "Convierte a hexadecimal"), ("to_binary", "Convierte a binario"), ("to_octets", "Convierte a lista de octetos"), ("bit_representation", "Representación visual de bits"), ("in_same_network", "¿Está en la misma red?"), ("is_subnet_of", "¿Está en esa subred?"), ("distance_to", "Distancia en direcciones"), ("hamming_distance", "Distancia de Hamming"), ("parent_network", "Red padre"), ("child_networks", "Subredes hijas"), ("sibling_networks", "Redes hermanas"), ("is_private", "¿Es IP privada?"), ("is_loopback", "¿Es loopback?"), ("is_multicast", "¿Es multicast?"), ("is_linklocal", "¿Es link-local?"), ("ip_class", "Clase de IP (A/B/C/D/E)"), ("reverse_dns_zone", "Zona DNS reversa"), ("ptr_record", "Registro PTR"), ("get_netmask_str", "Máscara como string"), ("get_prefix_length", "Longitud del prefijo CIDR"), ] # ========== MÉTODOS BÁSICOS EXISTENTES ========== def get_netmask_str(self) -> Optional[str]: return self._mask_obj.get_mask_str() if self._mask_obj else None def get_prefix_length(self) -> Optional[int]: return self._mask_obj.get_prefix_int() if self._mask_obj else None def NetworkAddress(self): prefix = self.get_prefix_length() if prefix is None: raise ValueError("No prefix/mask defined for NetworkAddress calculation.") mask_int = IP4Mask._prefix_to_mask_int(prefix) network_int = self._ip_int & mask_int parts = [ (network_int >> 24) & 0xFF, (network_int >> 16) & 0xFF, (network_int >> 8) & 0xFF, network_int & 0xFF ] network_str = '.'.join(str(x) for x in parts) return Class_IP4(network_str, prefix) def BroadcastAddress(self): prefix = self.get_prefix_length() if prefix is None: raise ValueError("No prefix/mask defined for BroadcastAddress calculation.") mask_int = IP4Mask._prefix_to_mask_int(prefix) broadcast_int = self._ip_int | (~mask_int & 0xFFFFFFFF) parts = [ (broadcast_int >> 24) & 0xFF, (broadcast_int >> 16) & 0xFF, (broadcast_int >> 8) & 0xFF, broadcast_int & 0xFF ] broadcast_str = '.'.join(str(x) for x in parts) return Class_IP4(broadcast_str, prefix) def Nodes(self): prefix = self.get_prefix_length() if prefix is None: raise ValueError("No prefix/mask defined for Nodes calculation.") if prefix >= 31: return 0 return (2 ** (32 - prefix)) - 2 def mask(self) -> Optional[IP4Mask]: """Retorna objeto máscara para autocompletado""" if not self._mask_obj: raise ValueError("No mask defined for this IP4 object.") return self._mask_obj # ========== ARITMÉTICA DE DIRECCIONES ========== def add_hosts(self, n: int) -> 'Class_IP4': """Suma N hosts a la dirección""" new_ip_int = (self._ip_int + n) & 0xFFFFFFFF new_parts = [ (new_ip_int >> 24) & 0xFF, (new_ip_int >> 16) & 0xFF, (new_ip_int >> 8) & 0xFF, new_ip_int & 0xFF ] new_ip_str = '.'.join(str(x) for x in new_parts) return Class_IP4(new_ip_str, self._mask_obj) def subtract_hosts(self, n: int) -> 'Class_IP4': """Resta N hosts a la dirección""" return self.add_hosts(-n) def next_ip(self) -> 'Class_IP4': """Siguiente dirección IP""" return self.add_hosts(1) def previous_ip(self) -> 'Class_IP4': """Dirección IP anterior""" return self.add_hosts(-1) def increment_octet(self, octet_num: int, value: int) -> 'Class_IP4': """Incrementa un octeto específico (1-4)""" if not 1 <= octet_num <= 4: raise ValueError("octet_num must be between 1 and 4") octets = self.to_octets() octets[octet_num - 1] = (octets[octet_num - 1] + value) % 256 new_ip_str = '.'.join(str(x) for x in octets) return Class_IP4(new_ip_str, self._mask_obj) def next_network(self) -> 'Class_IP4': """Próxima red del mismo tamaño""" if not self._mask_obj: raise ValueError("No mask defined for network calculation") network = self.NetworkAddress() network_size = 2 ** (32 - self._mask_obj.get_prefix_int()) next_net = network.add_hosts(network_size) return next_net.NetworkAddress() def previous_network(self) -> 'Class_IP4': """Red anterior del mismo tamaño""" if not self._mask_obj: raise ValueError("No mask defined for network calculation") network = self.NetworkAddress() network_size = 2 ** (32 - self._mask_obj.get_prefix_int()) prev_net = network.subtract_hosts(network_size) return prev_net.NetworkAddress() # ========== ANÁLISIS DE RED ========== def is_network_address(self) -> bool: """¿Es la dirección de red?""" if not self._mask_obj: return False return self._ip_int == self.NetworkAddress()._ip_int def is_broadcast_address(self) -> bool: """¿Es la dirección de broadcast?""" if not self._mask_obj: return False return self._ip_int == self.BroadcastAddress()._ip_int def is_host_address(self) -> bool: """¿Es una dirección de host válida?""" if not self._mask_obj: return True # Sin máscara, asumimos que es host return not (self.is_network_address() or self.is_broadcast_address()) def is_first_host(self) -> bool: """¿Es la primera IP asignable?""" if not self._mask_obj: return False return self._ip_int == self.first_host()._ip_int def is_last_host(self) -> bool: """¿Es la última IP asignable?""" if not self._mask_obj: return False return self._ip_int == self.last_host()._ip_int def first_host(self) -> 'Class_IP4': """Primera IP asignable""" if not self._mask_obj: raise ValueError("No mask defined") if self._mask_obj.get_prefix_int() >= 31: return self # En /31 y /32, la IP es el host return self.NetworkAddress().add_hosts(1) def last_host(self) -> 'Class_IP4': """Última IP asignable""" if not self._mask_obj: raise ValueError("No mask defined") if self._mask_obj.get_prefix_int() >= 31: return self # En /31 y /32, la IP es el host return self.BroadcastAddress().subtract_hosts(1) def network_range(self) -> Tuple['Class_IP4', 'Class_IP4']: """Rango de red (primera, última) como tupla""" return (self.first_host(), self.last_host()) def host_position(self) -> int: """¿En qué posición está dentro de la red?""" if not self._mask_obj: raise ValueError("No mask defined") network = self.NetworkAddress() return self._ip_int - network._ip_int # ========== CONVERSIONES AVANZADAS ========== def to_integer(self) -> int: """Convierte a entero""" return self._ip_int def to_hex(self) -> str: """Convierte a hexadecimal""" return f"0x{self._ip_int:08X}" def to_binary(self) -> str: """Convierte a binario""" return f"{self._ip_int:032b}" def to_octets(self) -> List[int]: """Convierte a lista de octetos""" return [ (self._ip_int >> 24) & 0xFF, (self._ip_int >> 16) & 0xFF, (self._ip_int >> 8) & 0xFF, self._ip_int & 0xFF ] def bit_representation(self) -> str: """Representación visual de bits""" binary = self.to_binary() return f"{binary[:8]}.{binary[8:16]}.{binary[16:24]}.{binary[24:32]}".replace('1', '█').replace('0', '.') def hamming_distance(self, other_ip: 'Class_IP4') -> int: """Distancia de Hamming (diferencia en bits)""" if not isinstance(other_ip, Class_IP4): raise TypeError("other_ip must be Class_IP4 instance") xor_result = self._ip_int ^ other_ip._ip_int return bin(xor_result).count('1') # ========== ANÁLISIS DE RELACIONES ========== def in_same_network(self, other_ip: 'Class_IP4') -> bool: """¿Están en la misma red?""" if not isinstance(other_ip, Class_IP4): raise TypeError("other_ip must be Class_IP4 instance") if not self._mask_obj: raise ValueError("No mask defined for network comparison") mask_int = IP4Mask._prefix_to_mask_int(self._mask_obj.get_prefix_int()) return (self._ip_int & mask_int) == (other_ip._ip_int & mask_int) def is_subnet_of(self, network_ip: 'Class_IP4') -> bool: """¿Esta IP está en esa red?""" if not isinstance(network_ip, Class_IP4): raise TypeError("network_ip must be Class_IP4 instance") if not network_ip._mask_obj: raise ValueError("network_ip must have a mask defined") mask_int = IP4Mask._prefix_to_mask_int(network_ip._mask_obj.get_prefix_int()) network_addr = network_ip._ip_int & mask_int this_network = self._ip_int & mask_int return network_addr == this_network def distance_to(self, other_ip: 'Class_IP4') -> int: """Distancia en direcciones""" if not isinstance(other_ip, Class_IP4): raise TypeError("other_ip must be Class_IP4 instance") return abs(self._ip_int - other_ip._ip_int) def parent_network(self, larger_mask: IP4Mask) -> 'Class_IP4': """Red padre con máscara mayor""" if not isinstance(larger_mask, IP4Mask): raise TypeError("larger_mask must be IP4Mask instance") if self._mask_obj and larger_mask.get_prefix_int() >= self._mask_obj.get_prefix_int(): raise ValueError("larger_mask must be less specific") mask_int = IP4Mask._prefix_to_mask_int(larger_mask.get_prefix_int()) parent_int = self._ip_int & mask_int parts = [ (parent_int >> 24) & 0xFF, (parent_int >> 16) & 0xFF, (parent_int >> 8) & 0xFF, parent_int & 0xFF ] parent_str = '.'.join(str(x) for x in parts) return Class_IP4(parent_str, larger_mask) def child_networks(self, smaller_mask: IP4Mask) -> List['Class_IP4']: """Subredes hijas""" if not isinstance(smaller_mask, IP4Mask): raise TypeError("smaller_mask must be IP4Mask instance") if not self._mask_obj: raise ValueError("IP must have a mask defined") if smaller_mask.get_prefix_int() <= self._mask_obj.get_prefix_int(): raise ValueError("smaller_mask must be more specific") network = self.NetworkAddress() subnet_count = 2 ** (smaller_mask.get_prefix_int() - self._mask_obj.get_prefix_int()) subnet_size = 2 ** (32 - smaller_mask.get_prefix_int()) subnets = [] current_ip = network._ip_int for i in range(subnet_count): parts = [ (current_ip >> 24) & 0xFF, (current_ip >> 16) & 0xFF, (current_ip >> 8) & 0xFF, current_ip & 0xFF ] subnet_str = '.'.join(str(x) for x in parts) subnets.append(Class_IP4(subnet_str, smaller_mask)) current_ip += subnet_size return subnets def sibling_networks(self) -> List['Class_IP4']: """Redes hermanas del mismo tamaño""" if not self._mask_obj: raise ValueError("IP must have a mask defined") if self._mask_obj.get_prefix_int() == 0: return [self] # /0 no tiene hermanas parent_mask = IP4Mask(self._mask_obj.get_prefix_int() - 1) parent = self.parent_network(parent_mask) return parent.child_networks(self._mask_obj) # ========== CLASIFICACIÓN DE IPS ========== def is_private(self) -> bool: """¿Es IP privada? (RFC 1918)""" # 10.0.0.0/8 if (self._ip_int & 0xFF000000) == 0x0A000000: return True # 172.16.0.0/12 if (self._ip_int & 0xFFF00000) == 0xAC100000: return True # 192.168.0.0/16 if (self._ip_int & 0xFFFF0000) == 0xC0A80000: return True return False def is_loopback(self) -> bool: """¿Es loopback? (127.0.0.0/8)""" return (self._ip_int & 0xFF000000) == 0x7F000000 def is_multicast(self) -> bool: """¿Es multicast? (224.0.0.0/4)""" return (self._ip_int & 0xF0000000) == 0xE0000000 def is_linklocal(self) -> bool: """¿Es link-local? (169.254.0.0/16)""" return (self._ip_int & 0xFFFF0000) == 0xA9FE0000 def ip_class(self) -> str: """Clase de IP (A/B/C/D/E) - legacy pero útil""" first_octet = (self._ip_int >> 24) & 0xFF if first_octet < 128: return "A" elif first_octet < 192: return "B" elif first_octet < 224: return "C" elif first_octet < 240: return "D" # Multicast else: return "E" # Experimental # ========== UTILIDADES DNS ========== def reverse_dns_zone(self) -> str: """Zona DNS reversa""" if not self._mask_obj: raise ValueError("No mask defined for reverse DNS zone") prefix = self._mask_obj.get_prefix_int() octets = self.to_octets() if prefix >= 24: return f"{octets[2]}.{octets[1]}.{octets[0]}.in-addr.arpa" elif prefix >= 16: return f"{octets[1]}.{octets[0]}.in-addr.arpa" elif prefix >= 8: return f"{octets[0]}.in-addr.arpa" else: return "in-addr.arpa" def ptr_record(self) -> str: """Registro PTR""" octets = self.to_octets() return f"{octets[3]}.{octets[2]}.{octets[1]}.{octets[0]}.in-addr.arpa" def generate_host_list(self, count: int) -> List['Class_IP4']: """Genera lista de IPs consecutivas""" if count <= 0: return [] result = [] current = self for i in range(count): result.append(current) current = current.next_ip() return result # ========== MÉTODOS ESTÁTICOS ========== @staticmethod def find_common_network(ip_list: List['Class_IP4']) -> 'Class_IP4': """Menor red que contiene todas las IPs""" if not ip_list: raise ValueError("ip_list cannot be empty") if len(ip_list) == 1: return ip_list[0] # Encontrar el prefijo común más largo min_ip = min(ip._ip_int for ip in ip_list) max_ip = max(ip._ip_int for ip in ip_list) # XOR para encontrar bits diferentes xor_result = min_ip ^ max_ip # Contar bits comunes desde la izquierda common_bits = 0 for i in range(32): if (xor_result >> (31 - i)) & 1: break common_bits += 1 # Crear la red común mask = IP4Mask(common_bits) network_int = min_ip & IP4Mask._prefix_to_mask_int(common_bits) parts = [ (network_int >> 24) & 0xFF, (network_int >> 16) & 0xFF, (network_int >> 8) & 0xFF, network_int & 0xFF ] network_str = '.'.join(str(x) for x in parts) return Class_IP4(network_str, mask) @staticmethod def detect_overlaps(network_list: List['Class_IP4']) -> List[Tuple['Class_IP4', 'Class_IP4']]: """Encontrar redes solapadas""" overlaps = [] for i, net1 in enumerate(network_list): for net2 in network_list[i+1:]: if net1.is_subnet_of(net2) or net2.is_subnet_of(net1): overlaps.append((net1, net2)) return overlaps @staticmethod def analyze_distribution(ip_list: List['Class_IP4']) -> dict: """Análisis estadístico de distribución""" if not ip_list: return {} classes = {"A": 0, "B": 0, "C": 0, "D": 0, "E": 0} private_count = 0 for ip in ip_list: classes[ip.ip_class()] += 1 if ip.is_private(): private_count += 1 return { "total": len(ip_list), "classes": classes, "private_count": private_count, "public_count": len(ip_list) - private_count, "common_network": Class_IP4.find_common_network(ip_list) }