A Python port of the Invisible Internet Project (I2P)
1"""Addresses — IP address validation and local address detection.
2
3Ported from net.i2p.util.Addresses.
4"""
5
6import ipaddress
7import socket
8from enum import Enum, auto
9from typing import Optional, Set
10
11
12class AddressType(Enum):
13 IPV4 = auto()
14 IPV6 = auto()
15 YGG = auto()
16
17
18class Addresses:
19 """Static utilities for IP address operations."""
20
21 @staticmethod
22 def is_ipv4(addr: str) -> bool:
23 """Check if string is a valid IPv4 address."""
24 try:
25 ipaddress.IPv4Address(addr)
26 return True
27 except (ipaddress.AddressValueError, ValueError):
28 return False
29
30 @staticmethod
31 def is_ipv6(addr: str) -> bool:
32 """Check if string is a valid IPv6 address."""
33 try:
34 ipaddress.IPv6Address(addr)
35 return True
36 except (ipaddress.AddressValueError, ValueError):
37 return False
38
39 @staticmethod
40 def is_ip_address(addr: str) -> bool:
41 """Check if string is a valid IP address (v4 or v6)."""
42 return Addresses.is_ipv4(addr) or Addresses.is_ipv6(addr)
43
44 @staticmethod
45 def get_ip(host: str) -> Optional[bytes]:
46 """Resolve hostname to IP bytes, or return None."""
47 try:
48 result = socket.getaddrinfo(host, None, socket.AF_UNSPEC, socket.SOCK_STREAM)
49 if result:
50 addr = result[0][4][0]
51 return ipaddress.ip_address(addr).packed
52 except (socket.gaierror, OSError, ValueError):
53 pass
54 return None
55
56 @staticmethod
57 def to_ip_string(ip_bytes: bytes) -> str:
58 """Convert IP bytes to string representation."""
59 return str(ipaddress.ip_address(ip_bytes))
60
61 @staticmethod
62 def is_public(addr: str) -> bool:
63 """Check if an IP address is publicly routable."""
64 try:
65 ip = ipaddress.ip_address(addr)
66 return ip.is_global
67 except ValueError:
68 return False
69
70 @staticmethod
71 def is_local(addr: str) -> bool:
72 """Check if an IP address is local/private."""
73 try:
74 ip = ipaddress.ip_address(addr)
75 return ip.is_private or ip.is_loopback or ip.is_link_local
76 except ValueError:
77 return False
78
79 @staticmethod
80 def get_addresses(include_local: bool = False,
81 include_ipv6: bool = False) -> set[str]:
82 """Get local network addresses.
83
84 Args:
85 include_local: include loopback and private addresses
86 include_ipv6: include IPv6 addresses
87
88 Returns:
89 Set of IP address strings.
90 """
91 addrs: set[str] = set()
92 try:
93 hostname = socket.gethostname()
94 for info in socket.getaddrinfo(hostname, None, socket.AF_UNSPEC, socket.SOCK_STREAM):
95 family, _, _, _, sockaddr = info
96 addr_str = sockaddr[0]
97 try:
98 ip = ipaddress.ip_address(addr_str)
99 except ValueError:
100 continue
101
102 if isinstance(ip, ipaddress.IPv6Address) and not include_ipv6:
103 continue
104 if not include_local and (ip.is_loopback or ip.is_link_local):
105 continue
106 addrs.add(str(ip))
107 except (socket.gaierror, OSError):
108 pass
109 return addrs
110
111 @staticmethod
112 def get_any_address() -> Optional[str]:
113 """Return first non-local IPv4 address, or None."""
114 addrs = Addresses.get_addresses(include_local=False, include_ipv6=False)
115 return next(iter(sorted(addrs)), None)
116
117 @staticmethod
118 def is_connected() -> bool:
119 """Check if we have any non-loopback address."""
120 return len(Addresses.get_addresses(include_local=False)) > 0
121
122 @staticmethod
123 def is_connected_ipv6() -> bool:
124 """Check if we have any non-loopback IPv6 address."""
125 for addr in Addresses.get_addresses(include_local=False, include_ipv6=True):
126 if ":" in addr:
127 return True
128 return False