Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
------

Expand Down
35 changes: 34 additions & 1 deletion multiaddr/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
]
102 changes: 102 additions & 0 deletions multiaddr/utils.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
1 change: 1 addition & 0 deletions newsfragments/116.feature
Original file line number Diff line number Diff line change
@@ -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.
66 changes: 66 additions & 0 deletions tests/test_ip_classification.py
Original file line number Diff line number Diff line change
@@ -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)
Loading