Skip to content

Commit a897b4a

Browse files
ambvsethmlarson
andauthored
[3.9] gh-122792: Make IPv4-mapped IPv6 address properties consistent with IPv4 (GH-122793) (GH-123819) (GH-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 <[email protected]>
1 parent 08830c7 commit a897b4a

File tree

3 files changed

+55
-5
lines changed

3 files changed

+55
-5
lines changed

Lib/ipaddress.py

+22-5
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ def collapse_addresses(addresses):
309309
[IPv4Network('192.0.2.0/24')]
310310
311311
Args:
312-
addresses: An iterator of IPv4Network or IPv6Network objects.
312+
addresses: An iterable of IPv4Network or IPv6Network objects.
313313
314314
Returns:
315315
An iterator of the collapsed IPv(4|6)Network objects.
@@ -1839,9 +1839,6 @@ def _string_from_ip_int(cls, ip_int=None):
18391839
def _explode_shorthand_ip_string(self):
18401840
"""Expand a shortened IPv6 address.
18411841
1842-
Args:
1843-
ip_str: A string, the IPv6 address.
1844-
18451842
Returns:
18461843
A string, the expanded IPv6 address.
18471844
@@ -1985,6 +1982,9 @@ def is_multicast(self):
19851982
See RFC 2373 2.7 for details.
19861983
19871984
"""
1985+
ipv4_mapped = self.ipv4_mapped
1986+
if ipv4_mapped is not None:
1987+
return ipv4_mapped.is_multicast
19881988
return self in self._constants._multicast_network
19891989

19901990
@property
@@ -1996,6 +1996,9 @@ def is_reserved(self):
19961996
reserved IPv6 Network ranges.
19971997
19981998
"""
1999+
ipv4_mapped = self.ipv4_mapped
2000+
if ipv4_mapped is not None:
2001+
return ipv4_mapped.is_reserved
19992002
return any(self in x for x in self._constants._reserved_networks)
20002003

20012004
@property
@@ -2006,6 +2009,9 @@ def is_link_local(self):
20062009
A boolean, True if the address is reserved per RFC 4291.
20072010
20082011
"""
2012+
ipv4_mapped = self.ipv4_mapped
2013+
if ipv4_mapped is not None:
2014+
return ipv4_mapped.is_link_local
20092015
return self in self._constants._linklocal_network
20102016

20112017
@property
@@ -2062,6 +2068,9 @@ def is_global(self):
20622068
``is_global`` has value opposite to :attr:`is_private`, except for the ``100.64.0.0/10``
20632069
IPv4 range where they are both ``False``.
20642070
"""
2071+
ipv4_mapped = self.ipv4_mapped
2072+
if ipv4_mapped is not None:
2073+
return ipv4_mapped.is_global
20652074
return not self.is_private
20662075

20672076
@property
@@ -2073,6 +2082,9 @@ def is_unspecified(self):
20732082
RFC 2373 2.5.2.
20742083
20752084
"""
2085+
ipv4_mapped = self.ipv4_mapped
2086+
if ipv4_mapped is not None:
2087+
return ipv4_mapped.is_unspecified
20762088
return self._ip == 0
20772089

20782090
@property
@@ -2084,6 +2096,9 @@ def is_loopback(self):
20842096
RFC 2373 2.5.3.
20852097
20862098
"""
2099+
ipv4_mapped = self.ipv4_mapped
2100+
if ipv4_mapped is not None:
2101+
return ipv4_mapped.is_loopback
20872102
return self._ip == 1
20882103

20892104
@property
@@ -2200,7 +2215,7 @@ def is_unspecified(self):
22002215

22012216
@property
22022217
def is_loopback(self):
2203-
return self._ip == 1 and self.network.is_loopback
2218+
return super().is_loopback and self.network.is_loopback
22042219

22052220

22062221
class IPv6Network(_BaseV6, _BaseNetwork):
@@ -2313,6 +2328,8 @@ class _IPv6Constants:
23132328
IPv6Network('2001:db8::/32'),
23142329
# IANA says N/A, let's consider it not globally reachable to be safe
23152330
IPv6Network('2002::/16'),
2331+
# RFC 9637: https://www.rfc-editor.org/rfc/rfc9637.html#section-6-2.2
2332+
IPv6Network('3fff::/20'),
23162333
IPv6Network('fc00::/7'),
23172334
IPv6Network('fe80::/10'),
23182335
]

Lib/test/test_ipaddress.py

+30
Original file line numberDiff line numberDiff line change
@@ -2415,6 +2415,36 @@ def testIpv4Mapped(self):
24152415
self.assertEqual(ipaddress.ip_address('::ffff:c0a8:101').ipv4_mapped,
24162416
ipaddress.ip_address('192.168.1.1'))
24172417

2418+
def testIpv4MappedProperties(self):
2419+
# Test that an IPv4 mapped IPv6 address has
2420+
# the same properties as an IPv4 address.
2421+
for addr4 in (
2422+
"178.62.3.251", # global
2423+
"169.254.169.254", # link local
2424+
"127.0.0.1", # loopback
2425+
"224.0.0.1", # multicast
2426+
"192.168.0.1", # private
2427+
"0.0.0.0", # unspecified
2428+
"100.64.0.1", # public and not global
2429+
):
2430+
with self.subTest(addr4):
2431+
ipv4 = ipaddress.IPv4Address(addr4)
2432+
ipv6 = ipaddress.IPv6Address(f"::ffff:{addr4}")
2433+
2434+
self.assertEqual(ipv4.is_global, ipv6.is_global)
2435+
self.assertEqual(ipv4.is_private, ipv6.is_private)
2436+
self.assertEqual(ipv4.is_reserved, ipv6.is_reserved)
2437+
self.assertEqual(ipv4.is_multicast, ipv6.is_multicast)
2438+
self.assertEqual(ipv4.is_unspecified, ipv6.is_unspecified)
2439+
self.assertEqual(ipv4.is_link_local, ipv6.is_link_local)
2440+
self.assertEqual(ipv4.is_loopback, ipv6.is_loopback)
2441+
2442+
def testIpv4MappedPrivateCheck(self):
2443+
self.assertEqual(
2444+
True, ipaddress.ip_address('::ffff:192.168.1.1').is_private)
2445+
self.assertEqual(
2446+
False, ipaddress.ip_address('::ffff:172.32.0.0').is_private)
2447+
24182448
def testAddrExclude(self):
24192449
addr1 = ipaddress.ip_network('10.1.1.0/24')
24202450
addr2 = ipaddress.ip_network('10.1.1.0/26')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Changed IPv4-mapped ``ipaddress.IPv6Address`` to consistently use the mapped IPv4
2+
address value for deciding properties. Properties which have their behavior fixed
3+
are ``is_multicast``, ``is_reserved``, ``is_link_local``, ``is_global``, and ``is_unspecified``.

0 commit comments

Comments
 (0)