Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions Lib/ipaddress.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
37 changes: 37 additions & 0 deletions Lib/test/test_ipaddress.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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``).
Loading