878 lines
33 KiB
Python
878 lines
33 KiB
Python
"""
|
|
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)
|
|
} |