From 5965bcb5d2642017fe4178bce6364bcadc00f99f Mon Sep 17 00:00:00 2001 From: Rinku Rajole Date: Wed, 14 Oct 2026 18:56:26 +0530 Subject: [PATCH] gh-107205: Align IPv4-mapped IPv6 networks with the embedded IPv4 properties --- Lib/ipaddress.py | 36 ++++++++++++++++++ Lib/test/test_ipaddress.py | 37 +++++++++++++++++++ ...-10-14-18-45-00.gh-issue-107205.m4Rt7w.rst | 4 ++ 3 files changed, 77 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-10-14-18-45-00.gh-issue-107205.m4Rt7w.rst diff --git a/Lib/ipaddress.py b/Lib/ipaddress.py index 8ac2a537c46deec..4254419d5187e3d 100644 --- a/Lib/ipaddress.py +++ b/Lib/ipaddress.py @@ -2430,6 +2430,42 @@ def is_site_local(self): return (self.network_address.is_site_local and self.broadcast_address.is_site_local) + def _ipv4_mapped_network(self): + # Subnets of ::ffff:0:0/96 follow the embedded IPv4 network + # (gh-107205). The /96 itself stays an IPv6 special-purpose block. + if self._prefixlen <= 96: + return None + start = self.network_address.ipv4_mapped + end = self.broadcast_address.ipv4_mapped + if start is None or end is None: + return None + return IPv4Network((start, self._prefixlen - 96)) + + @property + def is_private(self): + """Test if this network belongs to a private range. + + Subnets of the IPv4-mapped range follow the embedded IPv4 network + (gh-107205). ``::ffff:0:0/96`` itself stays private. + """ + mapped = self._ipv4_mapped_network() + if mapped is not None: + return mapped.is_private + return super().is_private + + @property + def is_global(self): + """Test if this network is allocated for public networks. + + Subnets of the IPv4-mapped range follow the embedded IPv4 network, + so shared address space (``100.64.0.0/10``) is neither private nor + global (gh-107205). + """ + mapped = self._ipv4_mapped_network() + if mapped is not None: + return mapped.is_global + return super().is_global + class _IPv6Constants: diff --git a/Lib/test/test_ipaddress.py b/Lib/test/test_ipaddress.py index 600e1f19a9cdc28..6fd57d05431063b 100644 --- a/Lib/test/test_ipaddress.py +++ b/Lib/test/test_ipaddress.py @@ -2587,6 +2587,43 @@ def testIpv4MappedProperties(self): self.assertEqual(ipv4.is_link_local, ipv6.is_link_local) self.assertEqual(ipv4.is_loopback, ipv6.is_loopback) + def testIpv4MappedNetworkProperties(self): + # gh-107205: a mapped address and its network must agree. Shared + # address space is neither private nor global. + samples = ( + "178.62.3.251", # global + "192.168.0.1", # private + "100.64.0.0", # shared address space + "100.64.0.1", + "8.8.8.8", + "127.0.0.1", + ) + for addr4 in samples: + with self.subTest(addr4): + ipv4 = ipaddress.ip_address(addr4) + ipv6 = ipaddress.ip_address(f"::ffff:{addr4}") + net6 = ipaddress.ip_network(f"::ffff:{addr4}") + self.assertEqual(ipv4.is_private, net6.is_private) + self.assertEqual(ipv4.is_global, net6.is_global) + self.assertEqual(ipv6.is_private, net6.is_private) + self.assertEqual(ipv6.is_global, net6.is_global) + + shared = ipaddress.ip_network("::ffff:100.64.0.0/106") + v4shared = ipaddress.ip_network("100.64.0.0/10") + self.assertEqual(shared.is_private, v4shared.is_private) + self.assertEqual(shared.is_global, v4shared.is_global) + self.assertFalse(shared.is_private) + self.assertFalse(shared.is_global) + + private = ipaddress.ip_network("::ffff:10.0.0.0/104") + self.assertTrue(private.is_private) + self.assertFalse(private.is_global) + public = ipaddress.ip_network("::ffff:8.8.8.0/120") + self.assertFalse(public.is_private) + self.assertTrue(public.is_global) + # The mapped range as a whole remains an IPv6 special-purpose block. + self.assertTrue(ipaddress.ip_network("::ffff:0:0/96").is_private) + def testIpv4MappedPrivateCheck(self): self.assertEqual( True, ipaddress.ip_address('::ffff:192.168.1.1').is_private) diff --git a/Misc/NEWS.d/next/Library/2026-10-14-18-45-00.gh-issue-107205.m4Rt7w.rst b/Misc/NEWS.d/next/Library/2026-10-14-18-45-00.gh-issue-107205.m4Rt7w.rst new file mode 100644 index 000000000000000..16d64a4d77b2cf5 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-14-18-45-00.gh-issue-107205.m4Rt7w.rst @@ -0,0 +1,4 @@ +:class:`ipaddress.IPv6Network` subnets of the IPv4-mapped range now use the +embedded IPv4 network for ``is_private`` and ``is_global``. A mapped +address and the ``/128`` network of that address no longer disagree, +including shared address space (``100.64.0.0/10``).