"""Addresses — IP address validation and local address detection. Ported from net.i2p.util.Addresses. """ import ipaddress import socket from enum import Enum, auto from typing import Optional, Set class AddressType(Enum): IPV4 = auto() IPV6 = auto() YGG = auto() class Addresses: """Static utilities for IP address operations.""" @staticmethod def is_ipv4(addr: str) -> bool: """Check if string is a valid IPv4 address.""" try: ipaddress.IPv4Address(addr) return True except (ipaddress.AddressValueError, ValueError): return False @staticmethod def is_ipv6(addr: str) -> bool: """Check if string is a valid IPv6 address.""" try: ipaddress.IPv6Address(addr) return True except (ipaddress.AddressValueError, ValueError): return False @staticmethod def is_ip_address(addr: str) -> bool: """Check if string is a valid IP address (v4 or v6).""" return Addresses.is_ipv4(addr) or Addresses.is_ipv6(addr) @staticmethod def get_ip(host: str) -> Optional[bytes]: """Resolve hostname to IP bytes, or return None.""" try: result = socket.getaddrinfo(host, None, socket.AF_UNSPEC, socket.SOCK_STREAM) if result: addr = result[0][4][0] return ipaddress.ip_address(addr).packed except (socket.gaierror, OSError, ValueError): pass return None @staticmethod def to_ip_string(ip_bytes: bytes) -> str: """Convert IP bytes to string representation.""" return str(ipaddress.ip_address(ip_bytes)) @staticmethod def is_public(addr: str) -> bool: """Check if an IP address is publicly routable.""" try: ip = ipaddress.ip_address(addr) return ip.is_global except ValueError: return False @staticmethod def is_local(addr: str) -> bool: """Check if an IP address is local/private.""" try: ip = ipaddress.ip_address(addr) return ip.is_private or ip.is_loopback or ip.is_link_local except ValueError: return False @staticmethod def get_addresses(include_local: bool = False, include_ipv6: bool = False) -> set[str]: """Get local network addresses. Args: include_local: include loopback and private addresses include_ipv6: include IPv6 addresses Returns: Set of IP address strings. """ addrs: set[str] = set() try: hostname = socket.gethostname() for info in socket.getaddrinfo(hostname, None, socket.AF_UNSPEC, socket.SOCK_STREAM): family, _, _, _, sockaddr = info addr_str = sockaddr[0] try: ip = ipaddress.ip_address(addr_str) except ValueError: continue if isinstance(ip, ipaddress.IPv6Address) and not include_ipv6: continue if not include_local and (ip.is_loopback or ip.is_link_local): continue addrs.add(str(ip)) except (socket.gaierror, OSError): pass return addrs @staticmethod def get_any_address() -> Optional[str]: """Return first non-local IPv4 address, or None.""" addrs = Addresses.get_addresses(include_local=False, include_ipv6=False) return next(iter(sorted(addrs)), None) @staticmethod def is_connected() -> bool: """Check if we have any non-loopback address.""" return len(Addresses.get_addresses(include_local=False)) > 0 @staticmethod def is_connected_ipv6() -> bool: """Check if we have any non-loopback IPv6 address.""" for addr in Addresses.get_addresses(include_local=False, include_ipv6=True): if ":" in addr: return True return False