From 2d87ec22268c2b0abe7d0b8de9d95f6ae19803ec Mon Sep 17 00:00:00 2001 From: sumanjeet0012 Date: Thu, 16 Jul 2026 20:39:16 +0530 Subject: [PATCH] feat: Add IP classification functions for Multiaddr objects --- README.rst | 19 ++++++ multiaddr/__init__.py | 35 ++++++++++- multiaddr/utils.py | 102 ++++++++++++++++++++++++++++++++ newsfragments/116.feature | 1 + tests/test_ip_classification.py | 66 +++++++++++++++++++++ 5 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 newsfragments/116.feature create mode 100644 tests/test_ip_classification.py diff --git a/README.rst b/README.rst index f706b85..3bd8c4f 100644 --- a/README.rst +++ b/README.rst @@ -40,6 +40,25 @@ Requirements Usage ===== +IP Classification +----------------- + +You can easily classify the IP components of multiaddrs (e.g. check for loopback, private networks, or NAT64): + +.. code-block:: python + + from multiaddr import Multiaddr, is_private_addr, is_ip_loopback + + ma1 = Multiaddr("/ip4/192.168.1.1/tcp/80") + print(is_private_addr(ma1)) # True + print(is_ip_loopback(ma1)) # False + + ma2 = Multiaddr("/ip4/127.0.0.1") + print(is_private_addr(ma2)) # True + print(is_ip_loopback(ma2)) # True + +Other available helpers include `is_public_addr`, `is_ip_unspecified`, `is_ip6_link_local`, `is_thin_waist`, and `is_nat64_ipv4_converted_ipv6_addr`. + Simple ------ diff --git a/multiaddr/__init__.py b/multiaddr/__init__.py index fef24a3..760512f 100755 --- a/multiaddr/__init__.py +++ b/multiaddr/__init__.py @@ -1,5 +1,38 @@ -from .multiaddr import Multiaddr # NOQA +from .multiaddr import Multiaddr __author__ = "Steven Buss" __email__ = "steven.buss@gmail.com" __version__ = "0.2.0" + +from .utils import ( + IP4_LOOPBACK, + IP4_UNSPECIFIED, + IP6_LOOPBACK, + IP6_UNSPECIFIED, + PRIVATE4, + PRIVATE6, + is_ip6_link_local, + is_ip_loopback, + is_ip_unspecified, + is_nat64_ipv4_converted_ipv6_addr, + is_private_addr, + is_public_addr, + is_thin_waist, +) + +__all__ = [ + "IP4_LOOPBACK", + "IP4_UNSPECIFIED", + "IP6_LOOPBACK", + "IP6_UNSPECIFIED", + "PRIVATE4", + "PRIVATE6", + "Multiaddr", + "is_ip6_link_local", + "is_ip_loopback", + "is_ip_unspecified", + "is_nat64_ipv4_converted_ipv6_addr", + "is_private_addr", + "is_public_addr", + "is_thin_waist", +] diff --git a/multiaddr/utils.py b/multiaddr/utils.py index f795038..a1cb843 100644 --- a/multiaddr/utils.py +++ b/multiaddr/utils.py @@ -1,9 +1,111 @@ +import ipaddress import socket from typing import Any import psutil from .multiaddr import Multiaddr +from .protocols import P_IP4, P_IP6, P_TCP, P_UDP + +IP4_LOOPBACK = Multiaddr("/ip4/127.0.0.1") +IP6_LOOPBACK = Multiaddr("/ip6/::1") +IP4_UNSPECIFIED = Multiaddr("/ip4/0.0.0.0") +IP6_UNSPECIFIED = Multiaddr("/ip6/::") + +PRIVATE4 = [ + ipaddress.ip_network(cidr) + for cidr in [ + "127.0.0.0/8", + "10.0.0.0/8", + "100.64.0.0/10", + "172.16.0.0/12", + "192.168.0.0/16", + "169.254.0.0/16", + ] +] + +PRIVATE6 = [ + ipaddress.ip_network(cidr) + for cidr in [ + "::1/128", + "fc00::/7", + "fe80::/10", + ] +] + + +def _get_ip(ma: Multiaddr) -> ipaddress.IPv4Address | ipaddress.IPv6Address | None: + protos = ma.protocols() + if not protos: + return None + first = protos[0] + if getattr(first, "code", None) in (P_IP4, P_IP6): + val = ma.value_for_protocol(getattr(first, "code", None)) + if val: + try: + return ipaddress.ip_address(val) + except ValueError: + pass + return None + + +def is_thin_waist(ma: Multiaddr) -> bool: + """Check if a multiaddr is a thin waist address (ip4/ip6 optionally followed by tcp/udp).""" + protos = ma.protocols() + if not protos: + return False + if getattr(protos[0], "code", None) not in (P_IP4, P_IP6): + return False + if len(protos) == 1: + return True + if len(protos) == 2 and getattr(protos[1], "code", None) in (P_TCP, P_UDP): + return True + return False + + +def is_ip_loopback(ma: Multiaddr) -> bool: + """Check if a multiaddr is a loopback IP address.""" + ip = _get_ip(ma) + return ip.is_loopback if ip else False + + +def is_ip_unspecified(ma: Multiaddr) -> bool: + """Check if a multiaddr is an unspecified IP address.""" + ip = _get_ip(ma) + return ip.is_unspecified if ip else False + + +def is_ip6_link_local(ma: Multiaddr) -> bool: + """Check if a multiaddr is an IPv6 link-local address.""" + ip = _get_ip(ma) + return ip.version == 6 and ip.is_link_local if ip else False + + +def is_private_addr(ma: Multiaddr) -> bool: + """Check if a multiaddr is a private IP address.""" + ip = _get_ip(ma) + if not ip: + return False + if ip.version == 4: + return any(ip in net for net in PRIVATE4) + else: + return any(ip in net for net in PRIVATE6) + + +def is_public_addr(ma: Multiaddr) -> bool: + """Check if a multiaddr is a public IP address.""" + ip = _get_ip(ma) + if not ip: + return False + return not is_ip_unspecified(ma) and not is_private_addr(ma) + + +def is_nat64_ipv4_converted_ipv6_addr(ma: Multiaddr) -> bool: + """Check if a multiaddr is a NAT64 converted IPv6 address.""" + ip = _get_ip(ma) + if not ip or ip.version != 6: + return False + return ip in ipaddress.ip_network("64:ff9b::/96") def is_wildcard(ip: str) -> bool: diff --git a/newsfragments/116.feature b/newsfragments/116.feature new file mode 100644 index 0000000..ab6f234 --- /dev/null +++ b/newsfragments/116.feature @@ -0,0 +1 @@ +Add IP classification helpers to `multiaddr.utils` (`is_ip_loopback`, `is_public_addr`, `is_private_addr`, etc.) that operate directly on `Multiaddr` objects. diff --git a/tests/test_ip_classification.py b/tests/test_ip_classification.py new file mode 100644 index 0000000..f666d65 --- /dev/null +++ b/tests/test_ip_classification.py @@ -0,0 +1,66 @@ +from multiaddr import Multiaddr +from multiaddr import utils + + +def test_is_thin_waist(): + assert utils.is_thin_waist(Multiaddr("/ip4/127.0.0.1")) + assert utils.is_thin_waist(Multiaddr("/ip4/127.0.0.1/tcp/80")) + assert utils.is_thin_waist(Multiaddr("/ip6/::1/udp/1234")) + assert not utils.is_thin_waist(Multiaddr("/ip4/127.0.0.1/tcp/80/ws")) + assert not utils.is_thin_waist(Multiaddr("/dns4/example.com")) + assert not utils.is_thin_waist(Multiaddr("/unix/a/b/c")) + + +def test_is_ip_loopback(): + assert utils.is_ip_loopback(Multiaddr("/ip4/127.0.0.1/tcp/80")) + assert utils.is_ip_loopback(Multiaddr("/ip6/::1")) + assert not utils.is_ip_loopback(Multiaddr("/ip4/1.2.3.4")) + assert not utils.is_ip_loopback(Multiaddr("/dns4/localhost")) + + +def test_is_ip_unspecified(): + assert utils.is_ip_unspecified(Multiaddr("/ip4/0.0.0.0/tcp/80")) + assert utils.is_ip_unspecified(Multiaddr("/ip6/::/udp/1234")) + assert not utils.is_ip_unspecified(Multiaddr("/ip4/127.0.0.1")) + + +def test_is_ip6_link_local(): + assert utils.is_ip6_link_local(Multiaddr("/ip6/fe80::1/tcp/80")) + assert not utils.is_ip6_link_local(Multiaddr("/ip6/::1")) + assert not utils.is_ip6_link_local(Multiaddr("/ip4/169.254.1.1")) # ipv4 + + +def test_is_private_addr(): + assert utils.is_private_addr(Multiaddr("/ip4/127.0.0.1")) + assert utils.is_private_addr(Multiaddr("/ip4/10.0.0.1/tcp/80")) + assert utils.is_private_addr(Multiaddr("/ip4/192.168.1.1")) + assert utils.is_private_addr(Multiaddr("/ip4/172.16.0.1")) + assert utils.is_private_addr(Multiaddr("/ip4/100.64.0.1")) + assert utils.is_private_addr(Multiaddr("/ip4/169.254.0.1")) + assert utils.is_private_addr(Multiaddr("/ip6/::1")) + assert utils.is_private_addr(Multiaddr("/ip6/fc00::1")) + assert utils.is_private_addr(Multiaddr("/ip6/fe80::1")) + assert not utils.is_private_addr(Multiaddr("/ip4/8.8.8.8")) + + +def test_is_public_addr(): + assert utils.is_public_addr(Multiaddr("/ip4/8.8.8.8/tcp/80")) + assert not utils.is_public_addr(Multiaddr("/ip4/127.0.0.1")) + assert not utils.is_public_addr(Multiaddr("/ip4/10.0.0.1")) + assert not utils.is_public_addr(Multiaddr("/ip4/0.0.0.0")) + + +def test_is_nat64_ipv4_converted_ipv6_addr(): + assert utils.is_nat64_ipv4_converted_ipv6_addr(Multiaddr("/ip6/64:ff9b::192.168.1.1")) + assert not utils.is_nat64_ipv4_converted_ipv6_addr(Multiaddr("/ip6/::1")) + assert not utils.is_nat64_ipv4_converted_ipv6_addr(Multiaddr("/ip4/192.168.1.1")) + + +def test_non_ip_addresses(): + ma = Multiaddr("/unix/var/run/docker.sock") + assert not utils.is_ip_loopback(ma) + assert not utils.is_ip_unspecified(ma) + assert not utils.is_ip6_link_local(ma) + assert not utils.is_private_addr(ma) + assert not utils.is_public_addr(ma) + assert not utils.is_nat64_ipv4_converted_ipv6_addr(ma)