Source code for nadzoring.network_base.router_ip

"""
Default gateway (router) IP address resolution for Linux and Windows.

On some Linux distributions the ``net-tools`` package must be installed
because the ``route`` command is not available by default::

    sudo apt install net-tools
"""

from ipaddress import AddressValueError, IPv4Address, IPv6Address
from logging import Logger
from platform import system
from socket import gaierror, gethostbyname
from subprocess import CalledProcessError, check_output

from nadzoring.logger import get_logger

logger: Logger = get_logger(__name__)


[docs] def get_ip_from_host(hostname: str) -> str: """ Resolve a hostname to an IP address, returning the input on failure. Args: hostname: Hostname or IP address string to resolve. Returns: Resolved IP address string, or ``hostname`` unchanged if resolution fails. """ try: return gethostbyname(hostname) except gaierror: return hostname
[docs] def _is_valid_ipv4(value: str) -> bool: """Return ``True`` if *value* is a syntactically valid IPv4 address.""" try: IPv4Address(value) except (AddressValueError, ValueError): return False return True
[docs] def _is_valid_ipv6(value: str) -> bool: """Return ``True`` if *value* is a syntactically valid IPv6 address.""" try: IPv6Address(value) except (AddressValueError, ValueError): return False return True
[docs] def check_ipv4(hostname: str) -> str: """ Return a resolved IPv4 address for *hostname*, or the input unchanged. If *hostname* is already a valid IPv4 address it is returned as-is. Otherwise a DNS lookup is attempted via :func:`get_ip_from_host`. Args: hostname: Hostname or IPv4 address string. Returns: IPv4 address string, or *hostname* unchanged when resolution fails. """ if _is_valid_ipv4(hostname): return hostname return get_ip_from_host(hostname)
[docs] def check_ipv6(hostname: str) -> str: """ Return a resolved IPv6 address for *hostname*, or the input unchanged. If *hostname* is already a valid IPv6 address it is returned as-is. Otherwise a DNS lookup is attempted via :func:`get_ip_from_host`. Args: hostname: Hostname or IPv6 address string. Returns: IPv6 address string, or *hostname* unchanged when resolution fails. """ if _is_valid_ipv6(hostname): return hostname return get_ip_from_host(hostname)
[docs] def _get_linux_router_ip(*, ipv6: bool) -> str | None: """Retrieve the default gateway address on Linux via ``route -n``.""" try: raw: str = ( check_output("route -n | grep UG", shell=True).decode().split()[1] # noqa: S602, S607 ) except (CalledProcessError, IndexError, OSError): logger.exception("Failed to retrieve router IP on Linux") return None return check_ipv6(raw) if ipv6 else check_ipv4(raw)
[docs] def _get_windows_router_ip(*, ipv6: bool) -> str | None: """Retrieve the default gateway address on Windows via ``route PRINT``.""" try: raw: str = ( check_output( # noqa: S602 "route PRINT 0* -4 | findstr 0.0.0.0", # noqa: S607 shell=True, ) .decode("cp866") .split()[-3] ) except (CalledProcessError, IndexError, OSError, UnicodeDecodeError): logger.exception("Failed to retrieve router IP on Windows") return None return check_ipv6(raw) if ipv6 else check_ipv4(raw)
[docs] def router_ip(*, ipv6: bool = False) -> str | None: """ Return the default router (gateway) IP address for the current system. Supports Linux (via ``route -n``) and Windows (via ``route PRINT``). The raw gateway value is validated and, if necessary, resolved from a hostname to an IP address. Args: ipv6: When ``True``, treat the gateway value as an IPv6 address. Defaults to ``False`` (IPv4). Returns: Gateway IP address string, or ``None`` when the gateway cannot be determined or the operating system is not supported. """ os_name: str = system() if os_name == "Linux": return _get_linux_router_ip(ipv6=ipv6) if os_name == "Windows": return _get_windows_router_ip(ipv6=ipv6) logger.warning("Unsupported operating system for router IP detection: %s", os_name) return None