]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
GH-113171: Fix "private" (non-global) IP address ranges (GH-113179)
authorJakub Stasiak <jakub@stasiak.at>
Fri, 22 Mar 2024 16:49:56 +0000 (17:49 +0100)
committerGitHub <noreply@github.com>
Fri, 22 Mar 2024 16:49:56 +0000 (17:49 +0100)
* 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.

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.

The _address_exclude_many() call returns 8 networks for IPv4, 121
networks for IPv6.

[1] https://github.com/python/cpython/issues/61602

Doc/library/ipaddress.rst
Doc/whatsnew/3.13.rst
Lib/ipaddress.py
Lib/test/test_ipaddress.py
Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst [new file with mode: 0644]

index 73f4960082617b84294c4f77da693b1100ed19a9..8f090b5eec598054a06b33e7cabe0d0e07ea3cc9 100644 (file)
@@ -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)
index 7e6c79dbf50aaca7fb955a449490f738bc6e3d98..bec788e7ed2b0e035254897c1c44af7fe3394675 100644 (file)
@@ -401,6 +401,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
 ---------
index 7d6edcf2478a82f0fd59e04860da23ef43117619..22cdfc93d8ad32d7da1b9aacccf504e0fd838630 100644 (file)
@@ -1086,7 +1086,11 @@ class _BaseNetwork(_IPAddressBase):
         """
         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):
@@ -1347,7 +1351,10 @@ class IPv4Address(_BaseV4, _BaseAddress):
         ``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)
+        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()
@@ -1578,13 +1585,15 @@ 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'),
+        IPv4Network('192.0.0.0/24'),
         IPv4Network('192.0.0.170/31'),
         IPv4Network('192.0.2.0/24'),
         IPv4Network('192.168.0.0/16'),
@@ -1595,6 +1604,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')
@@ -2086,7 +2100,10 @@ class IPv6Address(_BaseV6, _BaseAddress):
         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):
@@ -2342,19 +2359,31 @@ 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'),
         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'),
         ]
 
+    _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'),
index b4952acc2b61b1e5ec63f4680ed89e4e01ec98dc..f1519df673747a954e1a14cd43e25c7634c65f57 100644 (file)
@@ -2288,6 +2288,10 @@ class IpaddrUnitTest(unittest.TestCase):
         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)
@@ -2313,6 +2317,7 @@ class IpaddrUnitTest(unittest.TestCase):
         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)
@@ -2329,8 +2334,8 @@ class IpaddrUnitTest(unittest.TestCase):
         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(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)
@@ -2409,6 +2414,20 @@ class IpaddrUnitTest(unittest.TestCase):
         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)
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 (file)
index 0000000..f9a7247
--- /dev/null
@@ -0,0 +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.IPv6Address.is_private`
+* :attr:`ipaddress.IPv6Address.is_global`
+
+Also in the corresponding :class:`ipaddress.IPv4Network` and :class:`ipaddress.IPv6Network`
+attributes.