Skip to content

Commit 66af90f

Browse files
ambvsethmlarson
authored andcommitted
[3.9] pythongh-122792: Make IPv4-mapped IPv6 address properties consistent with IPv4 (pythonGH-122793) (pythonGH-123819) (pythonGH-127571)
Make IPv4-mapped IPv6 address properties consistent with IPv4. (cherry picked from commit 76a1c5d) (cherry picked from commit b58da40) Co-authored-by: Seth Michael Larson <seth@python.org>
1 parent 953a502 commit 66af90f

File tree

2 files changed

+52
-5
lines changed

2 files changed

+52
-5
lines changed

Lib/ipaddress.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ def collapse_addresses(addresses):
312312
[IPv4Network('192.0.2.0/24')]
313313
314314
Args:
315-
addresses: An iterator of IPv4Network or IPv6Network objects.
315+
addresses: An iterable of IPv4Network or IPv6Network objects.
316316
317317
Returns:
318318
An iterator of the collapsed IPv(4|6)Network objects.
@@ -1792,9 +1792,6 @@ def _string_from_ip_int(cls, ip_int=None):
17921792
def _explode_shorthand_ip_string(self):
17931793
"""Expand a shortened IPv6 address.
17941794
1795-
Args:
1796-
ip_str: A string, the IPv6 address.
1797-
17981795
Returns:
17991796
A string, the expanded IPv6 address.
18001797
@@ -1887,6 +1884,9 @@ def is_multicast(self):
18871884
See RFC 2373 2.7 for details.
18881885
18891886
"""
1887+
ipv4_mapped = self.ipv4_mapped
1888+
if ipv4_mapped is not None:
1889+
return ipv4_mapped.is_multicast
18901890
return self in self._constants._multicast_network
18911891

18921892
@property
@@ -1898,6 +1898,9 @@ def is_reserved(self):
18981898
reserved IPv6 Network ranges.
18991899
19001900
"""
1901+
ipv4_mapped = self.ipv4_mapped
1902+
if ipv4_mapped is not None:
1903+
return ipv4_mapped.is_reserved
19011904
return any(self in x for x in self._constants._reserved_networks)
19021905

19031906
@property
@@ -1908,6 +1911,9 @@ def is_link_local(self):
19081911
A boolean, True if the address is reserved per RFC 4291.
19091912
19101913
"""
1914+
ipv4_mapped = self.ipv4_mapped
1915+
if ipv4_mapped is not None:
1916+
return ipv4_mapped.is_link_local
19111917
return self in self._constants._linklocal_network
19121918

19131919
@property
@@ -1964,6 +1970,9 @@ def is_global(self):
19641970
``is_global`` has value opposite to :attr:`is_private`, except for the ``100.64.0.0/10``
19651971
IPv4 range where they are both ``False``.
19661972
"""
1973+
ipv4_mapped = self.ipv4_mapped
1974+
if ipv4_mapped is not None:
1975+
return ipv4_mapped.is_global
19671976
return not self.is_private
19681977

19691978
@property
@@ -1975,6 +1984,9 @@ def is_unspecified(self):
19751984
RFC 2373 2.5.2.
19761985
19771986
"""
1987+
ipv4_mapped = self.ipv4_mapped
1988+
if ipv4_mapped is not None:
1989+
return ipv4_mapped.is_unspecified
19781990
return self._ip == 0
19791991

19801992
@property
@@ -1986,6 +1998,9 @@ def is_loopback(self):
19861998
RFC 2373 2.5.3.
19871999
19882000
"""
2001+
ipv4_mapped = self.ipv4_mapped
2002+
if ipv4_mapped is not None:
2003+
return ipv4_mapped.is_loopback
19892004
return self._ip == 1
19902005

19912006
@property
@@ -2102,7 +2117,7 @@ def is_unspecified(self):
21022117

21032118
@property
21042119
def is_loopback(self):
2105-
return self._ip == 1 and self.network.is_loopback
2120+
return super().is_loopback and self.network.is_loopback
21062121

21072122

21082123
class IPv6Network(_BaseV6, _BaseNetwork):
@@ -2215,6 +2230,8 @@ class _IPv6Constants:
22152230
IPv6Network('2001:db8::/32'),
22162231
# IANA says N/A, let's consider it not globally reachable to be safe
22172232
IPv6Network('2002::/16'),
2233+
# RFC 9637: https://www.rfc-editor.org/rfc/rfc9637.html#section-6-2.2
2234+
IPv6Network('3fff::/20'),
22182235
IPv6Network('fc00::/7'),
22192236
IPv6Network('fe80::/10'),
22202237
]

Lib/test/test_ipaddress.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1913,6 +1913,36 @@ def testIpv4Mapped(self):
19131913
self.assertEqual(ipaddress.ip_address('::ffff:c0a8:101').ipv4_mapped,
19141914
ipaddress.ip_address('192.168.1.1'))
19151915

1916+
def testIpv4MappedProperties(self):
1917+
# Test that an IPv4 mapped IPv6 address has
1918+
# the same properties as an IPv4 address.
1919+
for addr4 in (
1920+
"178.62.3.251", # global
1921+
"169.254.169.254", # link local
1922+
"127.0.0.1", # loopback
1923+
"224.0.0.1", # multicast
1924+
"192.168.0.1", # private
1925+
"0.0.0.0", # unspecified
1926+
"100.64.0.1", # public and not global
1927+
):
1928+
with self.subTest(addr4):
1929+
ipv4 = ipaddress.IPv4Address(addr4)
1930+
ipv6 = ipaddress.IPv6Address(f"::ffff:{addr4}")
1931+
1932+
self.assertEqual(ipv4.is_global, ipv6.is_global)
1933+
self.assertEqual(ipv4.is_private, ipv6.is_private)
1934+
self.assertEqual(ipv4.is_reserved, ipv6.is_reserved)
1935+
self.assertEqual(ipv4.is_multicast, ipv6.is_multicast)
1936+
self.assertEqual(ipv4.is_unspecified, ipv6.is_unspecified)
1937+
self.assertEqual(ipv4.is_link_local, ipv6.is_link_local)
1938+
self.assertEqual(ipv4.is_loopback, ipv6.is_loopback)
1939+
1940+
def testIpv4MappedPrivateCheck(self):
1941+
self.assertEqual(
1942+
True, ipaddress.ip_address('::ffff:192.168.1.1').is_private)
1943+
self.assertEqual(
1944+
False, ipaddress.ip_address('::ffff:172.32.0.0').is_private)
1945+
19161946
def testAddrExclude(self):
19171947
addr1 = ipaddress.ip_network('10.1.1.0/24')
19181948
addr2 = ipaddress.ip_network('10.1.1.0/26')

0 commit comments

Comments
 (0)