From b83b9874a5bcfe6eb3233849371fb35ccf2fa60f Mon Sep 17 00:00:00 2001 From: Jakub Stasiak Date: Fri, 15 Dec 2023 14:47:19 +0100 Subject: [PATCH 1/8] GH-113171: Fix "private" (really non-global) IP address ranges The _private_networks variables, used by various is_private implementations, were missing some ranges and at the same time had overly strict ranges (where there are more specific ranges considered globally reachable by the IANA registries). This patch updates the ranges with what was missing or otherwise incorrect. A _address_exclude_many() helper function is created to calculate the necessary network ranges in the trickier cases. I left 100.64.0.0/10 alone, for now, as it's been made special in [1] and I'm not sure if we want to undo that as I don't quite understand the motivation behind it. Additionally I mainly focused on adding tests for IP*Address.is_global and I left IP*Network.is_global alone. The reasons for that are: * I don't think it makes much sense to have properties like is_global, is_private etc. on networks, where there are more than two possibilities (can be global, can be non-global, can be partially global). * The properties aren't documented for network objects in the first place so it's unclear what the semantics are The _address_exclude_many() call returns 8 networks for IPv4, 121 networks for IPv6. [1] https://github.com/python/cpython/issues/61602 --- Lib/ipaddress.py | 89 ++++++++++++++++++++++++++++++++++++-- Lib/test/test_ipaddress.py | 19 +++++++- 2 files changed, 103 insertions(+), 5 deletions(-) diff --git a/Lib/ipaddress.py b/Lib/ipaddress.py index e398cc138308d9..395173464e591f 100644 --- a/Lib/ipaddress.py +++ b/Lib/ipaddress.py @@ -12,6 +12,7 @@ import functools +from itertools import combinations IPV4LENGTH = 32 IPV6LENGTH = 128 @@ -1549,6 +1550,65 @@ def is_global(self): not self.is_private) +def _address_exclude_many(network, others): + """ + A version of IPv4Network/IPv6Network address_exclude() but for multiple networks + to exclude in the same call. + + The following are programming errors and will raise an exception: + + * Network type mismatch (IPv4 vs IPv6) + * Networks in `others` overlapping each other + * Networks in `others` not ovarlapping the provided `network` + + Returns: + A list of networks left after excluding `others` from `network`. + """ + # Precondition checks + for o in others: + if not network.overlaps(o): + raise AssertionError(f"No overlap between {network} and {o}") + + for a, b in combinations(others, 2): + if a.overlaps(b): + raise AssertionError(f"{a} overlaps {b}") + + networks = [network] + + for o in others: + networks = [ + result_network + for input_network in networks + for result_network in ( + input_network.address_exclude(o) + if input_network.overlaps(o) + else [input_network] + ) + ] + + # Integrity checks to make sure we haven't done something really wrong + addresses_started_with = network.num_addresses + addresses_excluded = sum(o.num_addresses for o in others) + addresses_left = sum(n.num_addresses for n in networks) + expected_addresses_left = addresses_started_with - addresses_excluded + + if addresses_left != expected_addresses_left: + raise AssertionError( + f"Should have {expected_addresses_left} addresses left, got {addresses_left}" + ) + + for n in networks: + for o in others: + if n.overlaps(o): + raise AssertionError(f"{n} overlaps {o}") + + for a, b in combinations(networks, 2): + if a.overlaps(b): + raise AssertionError(f'{a} overlaps {b}') + + return networks + + class _IPv4Constants: _linklocal_network = IPv4Network('169.254.0.0/16') @@ -1558,13 +1618,21 @@ class _IPv4Constants: _public_network = IPv4Network('100.64.0.0/10') + # Not globally reachable address blocks listed on + # https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml _private_networks = [ IPv4Network('0.0.0.0/8'), IPv4Network('10.0.0.0/8'), IPv4Network('127.0.0.0/8'), IPv4Network('169.254.0.0/16'), IPv4Network('172.16.0.0/12'), - IPv4Network('192.0.0.0/29'), + *_address_exclude_many( + IPv4Network('192.0.0.0/24'), + [ + IPv4Network('192.0.0.9/32'), + IPv4Network('192.0.0.10/32'), + ], + ), IPv4Network('192.0.0.170/31'), IPv4Network('192.0.2.0/24'), IPv4Network('192.168.0.0/16'), @@ -2310,15 +2378,28 @@ class _IPv6Constants: _multicast_network = IPv6Network('ff00::/8') + # Not globally reachable address blocks listed on + # https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml _private_networks = [ IPv6Network('::1/128'), IPv6Network('::/128'), IPv6Network('::ffff:0:0/96'), + IPv6Network('64:ff9b:1::/48'), IPv6Network('100::/64'), - IPv6Network('2001::/23'), - IPv6Network('2001:2::/48'), + *_address_exclude_many( + IPv6Network('2001::/23'), + [ + IPv6Network('2001:1::1/128'), + IPv6Network('2001:1::2/128'), + IPv6Network('2001:3::/32'), + IPv6Network('2001:4:112::/48'), + IPv6Network('2001:20::/28'), + IPv6Network('2001:30::/28'), + ], + ), IPv6Network('2001:db8::/32'), - IPv6Network('2001:10::/28'), + # IANA says N/A, let's consider it not globally reachable to be safe + IPv6Network('2002::/16'), IPv6Network('fc00::/7'), IPv6Network('fe80::/10'), ] diff --git a/Lib/test/test_ipaddress.py b/Lib/test/test_ipaddress.py index b4952acc2b61b1..41488a84975e17 100644 --- a/Lib/test/test_ipaddress.py +++ b/Lib/test/test_ipaddress.py @@ -2288,6 +2288,10 @@ def testReservedIpv4(self): self.assertEqual(True, ipaddress.ip_address( '172.31.255.255').is_private) self.assertEqual(False, ipaddress.ip_address('172.32.0.0').is_private) + self.assertFalse(ipaddress.ip_address('192.0.0.0').is_global) + self.assertTrue(ipaddress.ip_address('192.0.0.9').is_global) + self.assertTrue(ipaddress.ip_address('192.0.0.10').is_global) + self.assertFalse(ipaddress.ip_address('192.0.0.255').is_global) self.assertEqual(True, ipaddress.ip_address('169.254.100.200').is_link_local) @@ -2329,7 +2333,6 @@ def testPrivateNetworks(self): self.assertEqual(True, ipaddress.ip_network("::/128").is_private) self.assertEqual(True, ipaddress.ip_network("::ffff:0:0/96").is_private) self.assertEqual(True, ipaddress.ip_network("100::/64").is_private) - self.assertEqual(True, ipaddress.ip_network("2001::/23").is_private) self.assertEqual(True, ipaddress.ip_network("2001:2::/48").is_private) self.assertEqual(True, ipaddress.ip_network("2001:db8::/32").is_private) self.assertEqual(True, ipaddress.ip_network("2001:10::/28").is_private) @@ -2409,6 +2412,20 @@ def testReservedIpv6(self): self.assertEqual(True, ipaddress.ip_address('0::0').is_unspecified) self.assertEqual(False, ipaddress.ip_address('::1').is_unspecified) + self.assertFalse(ipaddress.ip_address('64:ff9b:1::').is_global) + self.assertFalse(ipaddress.ip_address('2001::').is_global) + self.assertTrue(ipaddress.ip_address('2001:1::1').is_global) + self.assertTrue(ipaddress.ip_address('2001:1::2').is_global) + self.assertFalse(ipaddress.ip_address('2001:2::').is_global) + self.assertTrue(ipaddress.ip_address('2001:3::').is_global) + self.assertFalse(ipaddress.ip_address('2001:4::').is_global) + self.assertTrue(ipaddress.ip_address('2001:4:112::').is_global) + self.assertFalse(ipaddress.ip_address('2001:10::').is_global) + self.assertTrue(ipaddress.ip_address('2001:20::').is_global) + self.assertTrue(ipaddress.ip_address('2001:30::').is_global) + self.assertFalse(ipaddress.ip_address('2001:40::').is_global) + self.assertFalse(ipaddress.ip_address('2002::').is_global) + # some generic IETF reserved addresses self.assertEqual(True, ipaddress.ip_address('100::').is_reserved) self.assertEqual(True, ipaddress.ip_network('4000::1/128').is_reserved) From 2a8d49b30b9f8f41f7ddab2d067101bf83e91bce Mon Sep 17 00:00:00 2001 From: Jakub Stasiak Date: Tue, 9 Jan 2024 20:38:18 +0100 Subject: [PATCH 2/8] Simplify --- Lib/ipaddress.py | 103 +++++++++++------------------------------------ 1 file changed, 24 insertions(+), 79 deletions(-) diff --git a/Lib/ipaddress.py b/Lib/ipaddress.py index 395173464e591f..f66ab94b55b7c2 100644 --- a/Lib/ipaddress.py +++ b/Lib/ipaddress.py @@ -1341,7 +1341,10 @@ def is_private(self): iana-ipv4-special-registry. """ - return any(self in net for net in self._constants._private_networks) + return ( + any(self in net for net in self._constants._private_networks) + and all(self not in net for net in self._constants._private_networks_exceptions) + ) @property @functools.lru_cache() @@ -1550,65 +1553,6 @@ def is_global(self): not self.is_private) -def _address_exclude_many(network, others): - """ - A version of IPv4Network/IPv6Network address_exclude() but for multiple networks - to exclude in the same call. - - The following are programming errors and will raise an exception: - - * Network type mismatch (IPv4 vs IPv6) - * Networks in `others` overlapping each other - * Networks in `others` not ovarlapping the provided `network` - - Returns: - A list of networks left after excluding `others` from `network`. - """ - # Precondition checks - for o in others: - if not network.overlaps(o): - raise AssertionError(f"No overlap between {network} and {o}") - - for a, b in combinations(others, 2): - if a.overlaps(b): - raise AssertionError(f"{a} overlaps {b}") - - networks = [network] - - for o in others: - networks = [ - result_network - for input_network in networks - for result_network in ( - input_network.address_exclude(o) - if input_network.overlaps(o) - else [input_network] - ) - ] - - # Integrity checks to make sure we haven't done something really wrong - addresses_started_with = network.num_addresses - addresses_excluded = sum(o.num_addresses for o in others) - addresses_left = sum(n.num_addresses for n in networks) - expected_addresses_left = addresses_started_with - addresses_excluded - - if addresses_left != expected_addresses_left: - raise AssertionError( - f"Should have {expected_addresses_left} addresses left, got {addresses_left}" - ) - - for n in networks: - for o in others: - if n.overlaps(o): - raise AssertionError(f"{n} overlaps {o}") - - for a, b in combinations(networks, 2): - if a.overlaps(b): - raise AssertionError(f'{a} overlaps {b}') - - return networks - - class _IPv4Constants: _linklocal_network = IPv4Network('169.254.0.0/16') @@ -1626,13 +1570,7 @@ class _IPv4Constants: IPv4Network('127.0.0.0/8'), IPv4Network('169.254.0.0/16'), IPv4Network('172.16.0.0/12'), - *_address_exclude_many( - IPv4Network('192.0.0.0/24'), - [ - IPv4Network('192.0.0.9/32'), - IPv4Network('192.0.0.10/32'), - ], - ), + IPv4Network('192.0.0.0/24'), IPv4Network('192.0.0.170/31'), IPv4Network('192.0.2.0/24'), IPv4Network('192.168.0.0/16'), @@ -1643,6 +1581,11 @@ class _IPv4Constants: IPv4Network('255.255.255.255/32'), ] + _private_networks_exceptions = [ + IPv4Network('192.0.0.9/32'), + IPv4Network('192.0.0.10/32'), + ] + _reserved_network = IPv4Network('240.0.0.0/4') _unspecified_address = IPv4Address('0.0.0.0') @@ -2128,7 +2071,10 @@ def is_private(self): ipv4_mapped = self.ipv4_mapped if ipv4_mapped is not None: return ipv4_mapped.is_private - return any(self in net for net in self._constants._private_networks) + return ( + any(self in net for net in self._constants._private_networks) + and all(self not in net for net in self._constants._private_networks_exceptions) + ) @property def is_global(self): @@ -2386,17 +2332,7 @@ class _IPv6Constants: IPv6Network('::ffff:0:0/96'), IPv6Network('64:ff9b:1::/48'), IPv6Network('100::/64'), - *_address_exclude_many( - IPv6Network('2001::/23'), - [ - IPv6Network('2001:1::1/128'), - IPv6Network('2001:1::2/128'), - IPv6Network('2001:3::/32'), - IPv6Network('2001:4:112::/48'), - IPv6Network('2001:20::/28'), - IPv6Network('2001:30::/28'), - ], - ), + IPv6Network('2001::/23'), IPv6Network('2001:db8::/32'), # IANA says N/A, let's consider it not globally reachable to be safe IPv6Network('2002::/16'), @@ -2404,6 +2340,15 @@ class _IPv6Constants: IPv6Network('fe80::/10'), ] + _private_networks_exceptions = [ + IPv6Network('2001:1::1/128'), + IPv6Network('2001:1::2/128'), + IPv6Network('2001:3::/32'), + IPv6Network('2001:4:112::/48'), + IPv6Network('2001:20::/28'), + IPv6Network('2001:30::/28'), + ] + _reserved_networks = [ IPv6Network('::/8'), IPv6Network('100::/8'), IPv6Network('200::/7'), IPv6Network('400::/6'), From 7c9235e5d41737cf07bf485d88ebba2bced52bd1 Mon Sep 17 00:00:00 2001 From: Jakub Stasiak Date: Tue, 9 Jan 2024 20:39:52 +0100 Subject: [PATCH 3/8] Remove an unnecessary import --- Lib/ipaddress.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/ipaddress.py b/Lib/ipaddress.py index f66ab94b55b7c2..60017afc77a96e 100644 --- a/Lib/ipaddress.py +++ b/Lib/ipaddress.py @@ -12,7 +12,6 @@ import functools -from itertools import combinations IPV4LENGTH = 32 IPV6LENGTH = 128 From d87ac9eb379b4ddd20a8696b24d36e3e6c80cb54 Mon Sep 17 00:00:00 2001 From: Jakub Stasiak Date: Thu, 14 Mar 2024 01:01:42 +0100 Subject: [PATCH 4/8] Update IP networks too --- Lib/ipaddress.py | 6 +++++- Lib/test/test_ipaddress.py | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Lib/ipaddress.py b/Lib/ipaddress.py index 60017afc77a96e..4a76b9866c01a7 100644 --- a/Lib/ipaddress.py +++ b/Lib/ipaddress.py @@ -1086,7 +1086,11 @@ def is_private(self): """ return any(self.network_address in priv_network and self.broadcast_address in priv_network - for priv_network in self._constants._private_networks) + for priv_network in self._constants._private_networks) and all( + self.network_address not in network and + self.broadcast_address not in network + for network in self._constants._private_networks_exceptions + ) @property def is_global(self): diff --git a/Lib/test/test_ipaddress.py b/Lib/test/test_ipaddress.py index 41488a84975e17..f1519df673747a 100644 --- a/Lib/test/test_ipaddress.py +++ b/Lib/test/test_ipaddress.py @@ -2317,6 +2317,7 @@ def testPrivateNetworks(self): self.assertEqual(True, ipaddress.ip_network("169.254.0.0/16").is_private) self.assertEqual(True, ipaddress.ip_network("172.16.0.0/12").is_private) self.assertEqual(True, ipaddress.ip_network("192.0.0.0/29").is_private) + self.assertEqual(False, ipaddress.ip_network("192.0.0.9/32").is_private) self.assertEqual(True, ipaddress.ip_network("192.0.0.170/31").is_private) self.assertEqual(True, ipaddress.ip_network("192.0.2.0/24").is_private) self.assertEqual(True, ipaddress.ip_network("192.168.0.0/16").is_private) @@ -2334,6 +2335,7 @@ def testPrivateNetworks(self): self.assertEqual(True, ipaddress.ip_network("::ffff:0:0/96").is_private) self.assertEqual(True, ipaddress.ip_network("100::/64").is_private) self.assertEqual(True, ipaddress.ip_network("2001:2::/48").is_private) + self.assertEqual(False, ipaddress.ip_network("2001:3::/48").is_private) self.assertEqual(True, ipaddress.ip_network("2001:db8::/32").is_private) self.assertEqual(True, ipaddress.ip_network("2001:10::/28").is_private) self.assertEqual(True, ipaddress.ip_network("fc00::/7").is_private) From f031f6e0643bf07283f5280cb709694e43275240 Mon Sep 17 00:00:00 2001 From: Jakub Stasiak Date: Mon, 18 Mar 2024 22:59:12 +0100 Subject: [PATCH 5/8] Add documentation --- Doc/library/ipaddress.rst | 16 ++++++++++++++++ Doc/whatsnew/3.13.rst | 2 ++ ...-03-14-01-38-44.gh-issue-113171.VFnObz.rst | 19 +++++++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst diff --git a/Doc/library/ipaddress.rst b/Doc/library/ipaddress.rst index 73f4960082617b..8f090b5eec5980 100644 --- a/Doc/library/ipaddress.rst +++ b/Doc/library/ipaddress.rst @@ -192,6 +192,18 @@ write code that handles both IP versions correctly. Address objects are ``is_private`` has value opposite to :attr:`is_global`, except for the shared address space (``100.64.0.0/10`` range) where they are both ``False``. + .. versionchanged:: 3.13 + + Fixed some false positives and false negatives. + + * ``192.0.0.0/24`` is considered private with the exception of ``192.0.0.9/32`` and + ``192.0.0.10/32`` (previously: only the ``192.0.0.0/29`` sub-range was considered private). + * ``64:ff9b:1::/48`` is considered private. + * ``2002::/16`` is considered private. + * There are exceptions within ``2001::/23`` (otherwise considered private): ``2001:1::1/128``, + ``2001:1::2/128``, ``2001:3::/32``, ``2001:4:112::/48``, ``2001:20::/28``, ``2001:30::/28``. + The exceptions are not considered private. + .. attribute:: is_global ``True`` if the address is defined as globally reachable by @@ -209,6 +221,10 @@ write code that handles both IP versions correctly. Address objects are .. versionadded:: 3.4 + .. versionchanged:: 3.13 + + Fixed some false positives and false negatives, see :attr:`is_private` for details. + .. attribute:: is_unspecified ``True`` if the address is unspecified. See :RFC:`5735` (for IPv4) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 0553cc97c5c75a..7912a9231ce275 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -371,6 +371,8 @@ ipaddress * Add the :attr:`ipaddress.IPv4Address.ipv6_mapped` property, which returns the IPv4-mapped IPv6 address. (Contributed by Charles Machalow in :gh:`109466`.) +* Fix ``is_global`` and ``is_private`` behavior in ``IPv4Address``, ``IPv6Address``, ``IPv4Network`` + and ``IPv6Network``. itertools --------- diff --git a/Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst b/Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst new file mode 100644 index 00000000000000..4632f880e0147d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst @@ -0,0 +1,19 @@ +Fixed various false positives and false negatives in + +* :attr:`ipaddress.IPv4Address.is_global` +* :attr:`ipaddress.IPv4Address.is_private` +* :attr:`ipaddress.IPv6Address.is_global` +* :attr:`ipaddress.IPv6Address.is_private` + +An the corresponding :class:`ipaddress.IPv4Network` and :class:`ipaddress.IPv6Network` +attributes. + +The new behavior: + +* ``192.0.0.0/24`` is considered private with the exception of ``192.0.0.9/32`` and + ``192.0.0.10/32`` (previously: only the ``192.0.0.0/29`` sub-range was considered private). +* ``64:ff9b:1::/48`` is considered private. +* ``2002::/16`` is considered private. +* There are exceptions within ``2001::/23`` (otherwise considered private): ``2001:1::1/128``, + ``2001:1::2/128``, ``2001:3::/32``, ``2001:4:112::/48``, ``2001:20::/28``, ``2001:30::/28``. + The exceptions are not considered private. From d3ee80d34bbac907e96b37ed64ae9252c71f36a0 Mon Sep 17 00:00:00 2001 From: Jakub Stasiak Date: Mon, 18 Mar 2024 23:15:47 +0100 Subject: [PATCH 6/8] Actually fix the shared address space too --- Doc/library/ipaddress.rst | 19 ++++---- Lib/ipaddress.py | 43 ++++++++----------- Lib/test/test_ipaddress.py | 2 +- ...-03-14-01-38-44.gh-issue-113171.VFnObz.rst | 2 + 4 files changed, 29 insertions(+), 37 deletions(-) diff --git a/Doc/library/ipaddress.rst b/Doc/library/ipaddress.rst index 8f090b5eec5980..00d02fff4cd2a4 100644 --- a/Doc/library/ipaddress.rst +++ b/Doc/library/ipaddress.rst @@ -180,17 +180,15 @@ write code that handles both IP versions correctly. Address objects are ``True`` if the address is defined as not globally reachable by iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_ - (for IPv6) with the following exceptions: + (for IPv6) with the following exception: - * ``is_private`` is ``False`` for the shared address space (``100.64.0.0/10``) - * For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the - semantics of the underlying IPv4 addresses and the following condition holds - (see :attr:`IPv6Address.ipv4_mapped`):: + For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the + semantics of the underlying IPv4 addresses and the following condition holds + (see :attr:`IPv6Address.ipv4_mapped`):: - address.is_private == address.ipv4_mapped.is_private + address.is_private == address.ipv4_mapped.is_private - ``is_private`` has value opposite to :attr:`is_global`, except for the shared address space - (``100.64.0.0/10`` range) where they are both ``False``. + ``is_private`` has value opposite to :attr:`is_global`. .. versionchanged:: 3.13 @@ -203,6 +201,8 @@ write code that handles both IP versions correctly. Address objects are * There are exceptions within ``2001::/23`` (otherwise considered private): ``2001:1::1/128``, ``2001:1::2/128``, ``2001:3::/32``, ``2001:4:112::/48``, ``2001:20::/28``, ``2001:30::/28``. The exceptions are not considered private. + * The shared address space (``100.64.0.0/10``) is not treated in any special way anymore, + previously it was considered both not private and not global. It is considered private now. .. attribute:: is_global @@ -216,8 +216,7 @@ write code that handles both IP versions correctly. Address objects are address.is_global == address.ipv4_mapped.is_global - ``is_global`` has value opposite to :attr:`is_private`, except for the shared address space - (``100.64.0.0/10`` range) where they are both ``False``. + ``is_global`` has value opposite to :attr:`is_private`. .. versionadded:: 3.4 diff --git a/Lib/ipaddress.py b/Lib/ipaddress.py index 22cdfc93d8ad32..f5307d37aa6f65 100644 --- a/Lib/ipaddress.py +++ b/Lib/ipaddress.py @@ -1339,17 +1339,15 @@ def is_reserved(self): def is_private(self): """``True`` if the address is defined as not globally reachable by iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_ - (for IPv6) with the following exceptions: + (for IPv6) with the following exception: - * ``is_private`` is ``False`` for ``100.64.0.0/10`` - * For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the - semantics of the underlying IPv4 addresses and the following condition holds - (see :attr:`IPv6Address.ipv4_mapped`):: + For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the + semantics of the underlying IPv4 addresses and the following condition holds + (see :attr:`IPv6Address.ipv4_mapped`):: - address.is_private == address.ipv4_mapped.is_private + address.is_private == address.ipv4_mapped.is_private - ``is_private`` has value opposite to :attr:`is_global`, except for the ``100.64.0.0/10`` - IPv4 range where they are both ``False``. + ``is_private`` has value opposite to :attr:`is_global`. """ return ( any(self in net for net in self._constants._private_networks) @@ -1369,10 +1367,9 @@ def is_global(self): address.is_global == address.ipv4_mapped.is_global - ``is_global`` has value opposite to :attr:`is_private`, except for the ``100.64.0.0/10`` - IPv4 range where they are both ``False``. + ``is_global`` has value opposite to :attr:`is_private`. """ - return self not in self._constants._public_network and not self.is_private + return not self.is_private @property def is_multicast(self): @@ -1571,9 +1568,7 @@ def is_global(self): iana-ipv4-special-registry. """ - return (not (self.network_address in IPv4Network('100.64.0.0/10') and - self.broadcast_address in IPv4Network('100.64.0.0/10')) and - not self.is_private) + return not self.is_private class _IPv4Constants: @@ -1583,13 +1578,12 @@ class _IPv4Constants: _multicast_network = IPv4Network('224.0.0.0/4') - _public_network = IPv4Network('100.64.0.0/10') - # Not globally reachable address blocks listed on # https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml _private_networks = [ IPv4Network('0.0.0.0/8'), IPv4Network('10.0.0.0/8'), + IPv4Network('100.64.0.0/10'), IPv4Network('127.0.0.0/8'), IPv4Network('169.254.0.0/16'), IPv4Network('172.16.0.0/12'), @@ -2085,17 +2079,15 @@ def is_site_local(self): def is_private(self): """``True`` if the address is defined as not globally reachable by iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_ - (for IPv6) with the following exceptions: + (for IPv6) with the following exception: - * ``is_private`` is ``False`` for ``100.64.0.0/10`` - * For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the - semantics of the underlying IPv4 addresses and the following condition holds - (see :attr:`IPv6Address.ipv4_mapped`):: + For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the + semantics of the underlying IPv4 addresses and the following condition holds + (see :attr:`IPv6Address.ipv4_mapped`):: - address.is_private == address.ipv4_mapped.is_private + address.is_private == address.ipv4_mapped.is_private - ``is_private`` has value opposite to :attr:`is_global`, except for the ``100.64.0.0/10`` - IPv4 range where they are both ``False``. + ``is_private`` has value opposite to :attr:`is_global`. """ ipv4_mapped = self.ipv4_mapped if ipv4_mapped is not None: @@ -2117,8 +2109,7 @@ def is_global(self): address.is_global == address.ipv4_mapped.is_global - ``is_global`` has value opposite to :attr:`is_private`, except for the ``100.64.0.0/10`` - IPv4 range where they are both ``False``. + ``is_global`` has value opposite to :attr:`is_private`. """ return not self.is_private diff --git a/Lib/test/test_ipaddress.py b/Lib/test/test_ipaddress.py index f1519df673747a..c0f1ecd8367aac 100644 --- a/Lib/test/test_ipaddress.py +++ b/Lib/test/test_ipaddress.py @@ -2263,7 +2263,7 @@ def testReservedIpv4(self): self.assertEqual(True, ipaddress.ip_network( '127.42.0.0/16').is_loopback) self.assertEqual(False, ipaddress.ip_network('128.0.0.0').is_loopback) - self.assertEqual(False, + self.assertEqual(True, ipaddress.ip_network('100.64.0.0/10').is_private) self.assertEqual(False, ipaddress.ip_network('100.64.0.0/10').is_global) diff --git a/Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst b/Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst index 4632f880e0147d..c1101a668ae76f 100644 --- a/Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst +++ b/Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst @@ -17,3 +17,5 @@ The new behavior: * There are exceptions within ``2001::/23`` (otherwise considered private): ``2001:1::1/128``, ``2001:1::2/128``, ``2001:3::/32``, ``2001:4:112::/48``, ``2001:20::/28``, ``2001:30::/28``. The exceptions are not considered private. +* The shared address space (``100.64.0.0/10``) is not treated in any special way anymore, + previously it was considered both not private and not global. It is considered private now. From 965e3a0ef9b5c28c4fcefbd3c0160b641cfe565e Mon Sep 17 00:00:00 2001 From: Jakub Stasiak Date: Fri, 22 Mar 2024 01:10:20 +0100 Subject: [PATCH 7/8] Revert "Actually fix the shared address space too" Leaving this for a separate PR (possibly). This reverts commit d3ee80d34bbac907e96b37ed64ae9252c71f36a0. --- Doc/library/ipaddress.rst | 19 ++++---- Lib/ipaddress.py | 43 +++++++++++-------- Lib/test/test_ipaddress.py | 2 +- ...-03-14-01-38-44.gh-issue-113171.VFnObz.rst | 2 - 4 files changed, 37 insertions(+), 29 deletions(-) diff --git a/Doc/library/ipaddress.rst b/Doc/library/ipaddress.rst index 00d02fff4cd2a4..8f090b5eec5980 100644 --- a/Doc/library/ipaddress.rst +++ b/Doc/library/ipaddress.rst @@ -180,15 +180,17 @@ write code that handles both IP versions correctly. Address objects are ``True`` if the address is defined as not globally reachable by iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_ - (for IPv6) with the following exception: + (for IPv6) with the following exceptions: - For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the - semantics of the underlying IPv4 addresses and the following condition holds - (see :attr:`IPv6Address.ipv4_mapped`):: + * ``is_private`` is ``False`` for the shared address space (``100.64.0.0/10``) + * For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the + semantics of the underlying IPv4 addresses and the following condition holds + (see :attr:`IPv6Address.ipv4_mapped`):: - address.is_private == address.ipv4_mapped.is_private + address.is_private == address.ipv4_mapped.is_private - ``is_private`` has value opposite to :attr:`is_global`. + ``is_private`` has value opposite to :attr:`is_global`, except for the shared address space + (``100.64.0.0/10`` range) where they are both ``False``. .. versionchanged:: 3.13 @@ -201,8 +203,6 @@ write code that handles both IP versions correctly. Address objects are * There are exceptions within ``2001::/23`` (otherwise considered private): ``2001:1::1/128``, ``2001:1::2/128``, ``2001:3::/32``, ``2001:4:112::/48``, ``2001:20::/28``, ``2001:30::/28``. The exceptions are not considered private. - * The shared address space (``100.64.0.0/10``) is not treated in any special way anymore, - previously it was considered both not private and not global. It is considered private now. .. attribute:: is_global @@ -216,7 +216,8 @@ write code that handles both IP versions correctly. Address objects are address.is_global == address.ipv4_mapped.is_global - ``is_global`` has value opposite to :attr:`is_private`. + ``is_global`` has value opposite to :attr:`is_private`, except for the shared address space + (``100.64.0.0/10`` range) where they are both ``False``. .. versionadded:: 3.4 diff --git a/Lib/ipaddress.py b/Lib/ipaddress.py index f5307d37aa6f65..22cdfc93d8ad32 100644 --- a/Lib/ipaddress.py +++ b/Lib/ipaddress.py @@ -1339,15 +1339,17 @@ def is_reserved(self): def is_private(self): """``True`` if the address is defined as not globally reachable by iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_ - (for IPv6) with the following exception: + (for IPv6) with the following exceptions: - For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the - semantics of the underlying IPv4 addresses and the following condition holds - (see :attr:`IPv6Address.ipv4_mapped`):: + * ``is_private`` is ``False`` for ``100.64.0.0/10`` + * For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the + semantics of the underlying IPv4 addresses and the following condition holds + (see :attr:`IPv6Address.ipv4_mapped`):: - address.is_private == address.ipv4_mapped.is_private + address.is_private == address.ipv4_mapped.is_private - ``is_private`` has value opposite to :attr:`is_global`. + ``is_private`` has value opposite to :attr:`is_global`, except for the ``100.64.0.0/10`` + IPv4 range where they are both ``False``. """ return ( any(self in net for net in self._constants._private_networks) @@ -1367,9 +1369,10 @@ def is_global(self): address.is_global == address.ipv4_mapped.is_global - ``is_global`` has value opposite to :attr:`is_private`. + ``is_global`` has value opposite to :attr:`is_private`, except for the ``100.64.0.0/10`` + IPv4 range where they are both ``False``. """ - return not self.is_private + return self not in self._constants._public_network and not self.is_private @property def is_multicast(self): @@ -1568,7 +1571,9 @@ def is_global(self): iana-ipv4-special-registry. """ - return not self.is_private + return (not (self.network_address in IPv4Network('100.64.0.0/10') and + self.broadcast_address in IPv4Network('100.64.0.0/10')) and + not self.is_private) class _IPv4Constants: @@ -1578,12 +1583,13 @@ class _IPv4Constants: _multicast_network = IPv4Network('224.0.0.0/4') + _public_network = IPv4Network('100.64.0.0/10') + # Not globally reachable address blocks listed on # https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml _private_networks = [ IPv4Network('0.0.0.0/8'), IPv4Network('10.0.0.0/8'), - IPv4Network('100.64.0.0/10'), IPv4Network('127.0.0.0/8'), IPv4Network('169.254.0.0/16'), IPv4Network('172.16.0.0/12'), @@ -2079,15 +2085,17 @@ def is_site_local(self): def is_private(self): """``True`` if the address is defined as not globally reachable by iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_ - (for IPv6) with the following exception: + (for IPv6) with the following exceptions: - For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the - semantics of the underlying IPv4 addresses and the following condition holds - (see :attr:`IPv6Address.ipv4_mapped`):: + * ``is_private`` is ``False`` for ``100.64.0.0/10`` + * For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the + semantics of the underlying IPv4 addresses and the following condition holds + (see :attr:`IPv6Address.ipv4_mapped`):: - address.is_private == address.ipv4_mapped.is_private + address.is_private == address.ipv4_mapped.is_private - ``is_private`` has value opposite to :attr:`is_global`. + ``is_private`` has value opposite to :attr:`is_global`, 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: @@ -2109,7 +2117,8 @@ def is_global(self): address.is_global == address.ipv4_mapped.is_global - ``is_global`` has value opposite to :attr:`is_private`. + ``is_global`` has value opposite to :attr:`is_private`, except for the ``100.64.0.0/10`` + IPv4 range where they are both ``False``. """ return not self.is_private diff --git a/Lib/test/test_ipaddress.py b/Lib/test/test_ipaddress.py index c0f1ecd8367aac..f1519df673747a 100644 --- a/Lib/test/test_ipaddress.py +++ b/Lib/test/test_ipaddress.py @@ -2263,7 +2263,7 @@ def testReservedIpv4(self): self.assertEqual(True, ipaddress.ip_network( '127.42.0.0/16').is_loopback) self.assertEqual(False, ipaddress.ip_network('128.0.0.0').is_loopback) - self.assertEqual(True, + self.assertEqual(False, ipaddress.ip_network('100.64.0.0/10').is_private) self.assertEqual(False, ipaddress.ip_network('100.64.0.0/10').is_global) diff --git a/Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst b/Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst index c1101a668ae76f..4632f880e0147d 100644 --- a/Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst +++ b/Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst @@ -17,5 +17,3 @@ The new behavior: * There are exceptions within ``2001::/23`` (otherwise considered private): ``2001:1::1/128``, ``2001:1::2/128``, ``2001:3::/32``, ``2001:4:112::/48``, ``2001:20::/28``, ``2001:30::/28``. The exceptions are not considered private. -* The shared address space (``100.64.0.0/10``) is not treated in any special way anymore, - previously it was considered both not private and not global. It is considered private now. From 5f41fffb6837307c83fa214039d93c4b54fb9ecd Mon Sep 17 00:00:00 2001 From: Jakub Stasiak Date: Fri, 22 Mar 2024 01:12:37 +0100 Subject: [PATCH 8/8] Make the news entry nicer --- ...024-03-14-01-38-44.gh-issue-113171.VFnObz.rst | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst b/Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst index 4632f880e0147d..f9a72473be4e2c 100644 --- a/Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst +++ b/Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst @@ -1,19 +1,9 @@ Fixed various false positives and false negatives in +* :attr:`ipaddress.IPv4Address.is_private` (see these docs for details) * :attr:`ipaddress.IPv4Address.is_global` -* :attr:`ipaddress.IPv4Address.is_private` -* :attr:`ipaddress.IPv6Address.is_global` * :attr:`ipaddress.IPv6Address.is_private` +* :attr:`ipaddress.IPv6Address.is_global` -An the corresponding :class:`ipaddress.IPv4Network` and :class:`ipaddress.IPv6Network` +Also in the corresponding :class:`ipaddress.IPv4Network` and :class:`ipaddress.IPv6Network` attributes. - -The new behavior: - -* ``192.0.0.0/24`` is considered private with the exception of ``192.0.0.9/32`` and - ``192.0.0.10/32`` (previously: only the ``192.0.0.0/29`` sub-range was considered private). -* ``64:ff9b:1::/48`` is considered private. -* ``2002::/16`` is considered private. -* There are exceptions within ``2001::/23`` (otherwise considered private): ``2001:1::1/128``, - ``2001:1::2/128``, ``2001:3::/32``, ``2001:4:112::/48``, ``2001:20::/28``, ``2001:30::/28``. - The exceptions are not considered private.