Skip to content

Commit

Permalink
[3.9] gh-122792: Make IPv4-mapped IPv6 address properties consistent …
Browse files Browse the repository at this point in the history
…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]>
  • Loading branch information
ambv and sethmlarson authored Dec 3, 2024
1 parent 08830c7 commit a897b4a
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 5 deletions.
27 changes: 22 additions & 5 deletions Lib/ipaddress.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ def collapse_addresses(addresses):
[IPv4Network('192.0.2.0/24')]
Args:
addresses: An iterator of IPv4Network or IPv6Network objects.
addresses: An iterable of IPv4Network or IPv6Network objects.
Returns:
An iterator of the collapsed IPv(4|6)Network objects.
Expand Down Expand Up @@ -1839,9 +1839,6 @@ def _string_from_ip_int(cls, ip_int=None):
def _explode_shorthand_ip_string(self):
"""Expand a shortened IPv6 address.
Args:
ip_str: A string, the IPv6 address.
Returns:
A string, the expanded IPv6 address.
Expand Down Expand Up @@ -1985,6 +1982,9 @@ def is_multicast(self):
See RFC 2373 2.7 for details.
"""
ipv4_mapped = self.ipv4_mapped
if ipv4_mapped is not None:
return ipv4_mapped.is_multicast
return self in self._constants._multicast_network

@property
Expand All @@ -1996,6 +1996,9 @@ def is_reserved(self):
reserved IPv6 Network ranges.
"""
ipv4_mapped = self.ipv4_mapped
if ipv4_mapped is not None:
return ipv4_mapped.is_reserved
return any(self in x for x in self._constants._reserved_networks)

@property
Expand All @@ -2006,6 +2009,9 @@ def is_link_local(self):
A boolean, True if the address is reserved per RFC 4291.
"""
ipv4_mapped = self.ipv4_mapped
if ipv4_mapped is not None:
return ipv4_mapped.is_link_local
return self in self._constants._linklocal_network

@property
Expand Down Expand Up @@ -2062,6 +2068,9 @@ def is_global(self):
``is_global`` has value opposite to :attr:`is_private`, except for the ``100.64.0.0/10``
IPv4 range where they are both ``False``.
"""
ipv4_mapped = self.ipv4_mapped
if ipv4_mapped is not None:
return ipv4_mapped.is_global
return not self.is_private

@property
Expand All @@ -2073,6 +2082,9 @@ def is_unspecified(self):
RFC 2373 2.5.2.
"""
ipv4_mapped = self.ipv4_mapped
if ipv4_mapped is not None:
return ipv4_mapped.is_unspecified
return self._ip == 0

@property
Expand All @@ -2084,6 +2096,9 @@ def is_loopback(self):
RFC 2373 2.5.3.
"""
ipv4_mapped = self.ipv4_mapped
if ipv4_mapped is not None:
return ipv4_mapped.is_loopback
return self._ip == 1

@property
Expand Down Expand Up @@ -2200,7 +2215,7 @@ def is_unspecified(self):

@property
def is_loopback(self):
return self._ip == 1 and self.network.is_loopback
return super().is_loopback and self.network.is_loopback


class IPv6Network(_BaseV6, _BaseNetwork):
Expand Down Expand Up @@ -2313,6 +2328,8 @@ class _IPv6Constants:
IPv6Network('2001:db8::/32'),
# IANA says N/A, let's consider it not globally reachable to be safe
IPv6Network('2002::/16'),
# RFC 9637: https://www.rfc-editor.org/rfc/rfc9637.html#section-6-2.2
IPv6Network('3fff::/20'),
IPv6Network('fc00::/7'),
IPv6Network('fe80::/10'),
]
Expand Down
30 changes: 30 additions & 0 deletions Lib/test/test_ipaddress.py
Original file line number Diff line number Diff line change
Expand Up @@ -2415,6 +2415,36 @@ def testIpv4Mapped(self):
self.assertEqual(ipaddress.ip_address('::ffff:c0a8:101').ipv4_mapped,
ipaddress.ip_address('192.168.1.1'))

def testIpv4MappedProperties(self):
# Test that an IPv4 mapped IPv6 address has
# the same properties as an IPv4 address.
for addr4 in (
"178.62.3.251", # global
"169.254.169.254", # link local
"127.0.0.1", # loopback
"224.0.0.1", # multicast
"192.168.0.1", # private
"0.0.0.0", # unspecified
"100.64.0.1", # public and not global
):
with self.subTest(addr4):
ipv4 = ipaddress.IPv4Address(addr4)
ipv6 = ipaddress.IPv6Address(f"::ffff:{addr4}")

self.assertEqual(ipv4.is_global, ipv6.is_global)
self.assertEqual(ipv4.is_private, ipv6.is_private)
self.assertEqual(ipv4.is_reserved, ipv6.is_reserved)
self.assertEqual(ipv4.is_multicast, ipv6.is_multicast)
self.assertEqual(ipv4.is_unspecified, ipv6.is_unspecified)
self.assertEqual(ipv4.is_link_local, ipv6.is_link_local)
self.assertEqual(ipv4.is_loopback, ipv6.is_loopback)

def testIpv4MappedPrivateCheck(self):
self.assertEqual(
True, ipaddress.ip_address('::ffff:192.168.1.1').is_private)
self.assertEqual(
False, ipaddress.ip_address('::ffff:172.32.0.0').is_private)

def testAddrExclude(self):
addr1 = ipaddress.ip_network('10.1.1.0/24')
addr2 = ipaddress.ip_network('10.1.1.0/26')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Changed IPv4-mapped ``ipaddress.IPv6Address`` to consistently use the mapped IPv4
address value for deciding properties. Properties which have their behavior fixed
are ``is_multicast``, ``is_reserved``, ``is_link_local``, ``is_global``, and ``is_unspecified``.

0 comments on commit a897b4a

Please sign in to comment.