Skip to content

Commit c62c9e5

Browse files
encukoujstasiak
andauthored
[3.10] gh-113171: gh-65056: Fix "private" (non-global) IP address ranges (GH-113179) (GH-113186) (GH-118177) (GH-118229)
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. 100.64.0.0/10 is left alone, for now, as it's been made special in [1]. The _address_exclude_many() call returns 8 networks for IPv4, 121 networks for IPv6. [1] #61602 In 3.10 and below, is_private checks whether the network and broadcast address are both private. In later versions (where the test wss backported from), it checks whether they both are in the same private network. For 0.0.0.0/0, both 0.0.0.0 and 255.225.255.255 are private, but one is in 0.0.0.0/8 ("This network") and the other in 255.255.255.255/32 ("Limited broadcast"). --------- Co-authored-by: Jakub Stasiak <jakub@stasiak.at>
1 parent 2ec7018 commit c62c9e5

File tree

5 files changed

+184
-22
lines changed

5 files changed

+184
-22
lines changed

Doc/library/ipaddress.rst

+39-4
Original file line numberDiff line numberDiff line change
@@ -188,18 +188,53 @@ write code that handles both IP versions correctly. Address objects are
188188

189189
.. attribute:: is_private
190190

191-
``True`` if the address is allocated for private networks. See
191+
``True`` if the address is defined as not globally reachable by
192192
iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
193-
(for IPv6).
193+
(for IPv6) with the following exceptions:
194+
195+
* ``is_private`` is ``False`` for the shared address space (``100.64.0.0/10``)
196+
* For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
197+
semantics of the underlying IPv4 addresses and the following condition holds
198+
(see :attr:`IPv6Address.ipv4_mapped`)::
199+
200+
address.is_private == address.ipv4_mapped.is_private
201+
202+
``is_private`` has value opposite to :attr:`is_global`, except for the shared address space
203+
(``100.64.0.0/10`` range) where they are both ``False``.
204+
205+
.. versionchanged:: 3.10.15
206+
207+
Fixed some false positives and false negatives.
208+
209+
* ``192.0.0.0/24`` is considered private with the exception of ``192.0.0.9/32`` and
210+
``192.0.0.10/32`` (previously: only the ``192.0.0.0/29`` sub-range was considered private).
211+
* ``64:ff9b:1::/48`` is considered private.
212+
* ``2002::/16`` is considered private.
213+
* There are exceptions within ``2001::/23`` (otherwise considered private): ``2001:1::1/128``,
214+
``2001:1::2/128``, ``2001:3::/32``, ``2001:4:112::/48``, ``2001:20::/28``, ``2001:30::/28``.
215+
The exceptions are not considered private.
194216

195217
.. attribute:: is_global
196218

197-
``True`` if the address is allocated for public networks. See
219+
``True`` if the address is defined as globally reachable by
198220
iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
199-
(for IPv6).
221+
(for IPv6) with the following exception:
222+
223+
For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
224+
semantics of the underlying IPv4 addresses and the following condition holds
225+
(see :attr:`IPv6Address.ipv4_mapped`)::
226+
227+
address.is_global == address.ipv4_mapped.is_global
228+
229+
``is_global`` has value opposite to :attr:`is_private`, except for the shared address space
230+
(``100.64.0.0/10`` range) where they are both ``False``.
200231

201232
.. versionadded:: 3.4
202233

234+
.. versionchanged:: 3.10.15
235+
236+
Fixed some false positives and false negatives, see :attr:`is_private` for details.
237+
203238
.. attribute:: is_unspecified
204239

205240
``True`` if the address is unspecified. See :RFC:`5735` (for IPv4)

Doc/whatsnew/3.10.rst

+9
Original file line numberDiff line numberDiff line change
@@ -2348,3 +2348,12 @@ tarfile
23482348
:exc:`DeprecationWarning`.
23492349
In Python 3.14, the default will switch to ``'data'``.
23502350
(Contributed by Petr Viktorin in :pep:`706`.)
2351+
2352+
Notable changes in 3.10.15
2353+
==========================
2354+
2355+
ipaddress
2356+
---------
2357+
2358+
* Fixed ``is_global`` and ``is_private`` behavior in ``IPv4Address``,
2359+
``IPv6Address``, ``IPv4Network`` and ``IPv6Network``.

Lib/ipaddress.py

+75-18
Original file line numberDiff line numberDiff line change
@@ -1323,18 +1323,41 @@ def is_reserved(self):
13231323
@property
13241324
@functools.lru_cache()
13251325
def is_private(self):
1326-
"""Test if this address is allocated for private networks.
1326+
"""``True`` if the address is defined as not globally reachable by
1327+
iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
1328+
(for IPv6) with the following exceptions:
13271329
1328-
Returns:
1329-
A boolean, True if the address is reserved per
1330-
iana-ipv4-special-registry.
1330+
* ``is_private`` is ``False`` for ``100.64.0.0/10``
1331+
* For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
1332+
semantics of the underlying IPv4 addresses and the following condition holds
1333+
(see :attr:`IPv6Address.ipv4_mapped`)::
1334+
1335+
address.is_private == address.ipv4_mapped.is_private
13311336
1337+
``is_private`` has value opposite to :attr:`is_global`, except for the ``100.64.0.0/10``
1338+
IPv4 range where they are both ``False``.
13321339
"""
1333-
return any(self in net for net in self._constants._private_networks)
1340+
return (
1341+
any(self in net for net in self._constants._private_networks)
1342+
and all(self not in net for net in self._constants._private_networks_exceptions)
1343+
)
13341344

13351345
@property
13361346
@functools.lru_cache()
13371347
def is_global(self):
1348+
"""``True`` if the address is defined as globally reachable by
1349+
iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
1350+
(for IPv6) with the following exception:
1351+
1352+
For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
1353+
semantics of the underlying IPv4 addresses and the following condition holds
1354+
(see :attr:`IPv6Address.ipv4_mapped`)::
1355+
1356+
address.is_global == address.ipv4_mapped.is_global
1357+
1358+
``is_global`` has value opposite to :attr:`is_private`, except for the ``100.64.0.0/10``
1359+
IPv4 range where they are both ``False``.
1360+
"""
13381361
return self not in self._constants._public_network and not self.is_private
13391362

13401363
@property
@@ -1538,13 +1561,15 @@ class _IPv4Constants:
15381561

15391562
_public_network = IPv4Network('100.64.0.0/10')
15401563

1564+
# Not globally reachable address blocks listed on
1565+
# https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
15411566
_private_networks = [
15421567
IPv4Network('0.0.0.0/8'),
15431568
IPv4Network('10.0.0.0/8'),
15441569
IPv4Network('127.0.0.0/8'),
15451570
IPv4Network('169.254.0.0/16'),
15461571
IPv4Network('172.16.0.0/12'),
1547-
IPv4Network('192.0.0.0/29'),
1572+
IPv4Network('192.0.0.0/24'),
15481573
IPv4Network('192.0.0.170/31'),
15491574
IPv4Network('192.0.2.0/24'),
15501575
IPv4Network('192.168.0.0/16'),
@@ -1555,6 +1580,11 @@ class _IPv4Constants:
15551580
IPv4Network('255.255.255.255/32'),
15561581
]
15571582

1583+
_private_networks_exceptions = [
1584+
IPv4Network('192.0.0.9/32'),
1585+
IPv4Network('192.0.0.10/32'),
1586+
]
1587+
15581588
_reserved_network = IPv4Network('240.0.0.0/4')
15591589

15601590
_unspecified_address = IPv4Address('0.0.0.0')
@@ -1996,27 +2026,42 @@ def is_site_local(self):
19962026
@property
19972027
@functools.lru_cache()
19982028
def is_private(self):
1999-
"""Test if this address is allocated for private networks.
2029+
"""``True`` if the address is defined as not globally reachable by
2030+
iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
2031+
(for IPv6) with the following exceptions:
20002032
2001-
Returns:
2002-
A boolean, True if the address is reserved per
2003-
iana-ipv6-special-registry, or is ipv4_mapped and is
2004-
reserved in the iana-ipv4-special-registry.
2033+
* ``is_private`` is ``False`` for ``100.64.0.0/10``
2034+
* For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
2035+
semantics of the underlying IPv4 addresses and the following condition holds
2036+
(see :attr:`IPv6Address.ipv4_mapped`)::
2037+
2038+
address.is_private == address.ipv4_mapped.is_private
20052039
2040+
``is_private`` has value opposite to :attr:`is_global`, except for the ``100.64.0.0/10``
2041+
IPv4 range where they are both ``False``.
20062042
"""
20072043
ipv4_mapped = self.ipv4_mapped
20082044
if ipv4_mapped is not None:
20092045
return ipv4_mapped.is_private
2010-
return any(self in net for net in self._constants._private_networks)
2046+
return (
2047+
any(self in net for net in self._constants._private_networks)
2048+
and all(self not in net for net in self._constants._private_networks_exceptions)
2049+
)
20112050

20122051
@property
20132052
def is_global(self):
2014-
"""Test if this address is allocated for public networks.
2053+
"""``True`` if the address is defined as globally reachable by
2054+
iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
2055+
(for IPv6) with the following exception:
20152056
2016-
Returns:
2017-
A boolean, true if the address is not reserved per
2018-
iana-ipv6-special-registry.
2057+
For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
2058+
semantics of the underlying IPv4 addresses and the following condition holds
2059+
(see :attr:`IPv6Address.ipv4_mapped`)::
2060+
2061+
address.is_global == address.ipv4_mapped.is_global
20192062
2063+
``is_global`` has value opposite to :attr:`is_private`, except for the ``100.64.0.0/10``
2064+
IPv4 range where they are both ``False``.
20202065
"""
20212066
return not self.is_private
20222067

@@ -2257,19 +2302,31 @@ class _IPv6Constants:
22572302

22582303
_multicast_network = IPv6Network('ff00::/8')
22592304

2305+
# Not globally reachable address blocks listed on
2306+
# https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
22602307
_private_networks = [
22612308
IPv6Network('::1/128'),
22622309
IPv6Network('::/128'),
22632310
IPv6Network('::ffff:0:0/96'),
2311+
IPv6Network('64:ff9b:1::/48'),
22642312
IPv6Network('100::/64'),
22652313
IPv6Network('2001::/23'),
2266-
IPv6Network('2001:2::/48'),
22672314
IPv6Network('2001:db8::/32'),
2268-
IPv6Network('2001:10::/28'),
2315+
# IANA says N/A, let's consider it not globally reachable to be safe
2316+
IPv6Network('2002::/16'),
22692317
IPv6Network('fc00::/7'),
22702318
IPv6Network('fe80::/10'),
22712319
]
22722320

2321+
_private_networks_exceptions = [
2322+
IPv6Network('2001:1::1/128'),
2323+
IPv6Network('2001:1::2/128'),
2324+
IPv6Network('2001:3::/32'),
2325+
IPv6Network('2001:4:112::/48'),
2326+
IPv6Network('2001:20::/28'),
2327+
IPv6Network('2001:30::/28'),
2328+
]
2329+
22732330
_reserved_networks = [
22742331
IPv6Network('::/8'), IPv6Network('100::/8'),
22752332
IPv6Network('200::/7'), IPv6Network('400::/6'),

Lib/test/test_ipaddress.py

+52
Original file line numberDiff line numberDiff line change
@@ -2263,6 +2263,10 @@ def testReservedIpv4(self):
22632263
self.assertEqual(True, ipaddress.ip_address(
22642264
'172.31.255.255').is_private)
22652265
self.assertEqual(False, ipaddress.ip_address('172.32.0.0').is_private)
2266+
self.assertFalse(ipaddress.ip_address('192.0.0.0').is_global)
2267+
self.assertTrue(ipaddress.ip_address('192.0.0.9').is_global)
2268+
self.assertTrue(ipaddress.ip_address('192.0.0.10').is_global)
2269+
self.assertFalse(ipaddress.ip_address('192.0.0.255').is_global)
22662270

22672271
self.assertEqual(True,
22682272
ipaddress.ip_address('169.254.100.200').is_link_local)
@@ -2278,6 +2282,40 @@ def testReservedIpv4(self):
22782282
self.assertEqual(False, ipaddress.ip_address('128.0.0.0').is_loopback)
22792283
self.assertEqual(True, ipaddress.ip_network('0.0.0.0').is_unspecified)
22802284

2285+
def testPrivateNetworks(self):
2286+
self.assertEqual(True, ipaddress.ip_network("0.0.0.0/0").is_private)
2287+
self.assertEqual(False, ipaddress.ip_network("1.0.0.0/8").is_private)
2288+
2289+
self.assertEqual(True, ipaddress.ip_network("0.0.0.0/8").is_private)
2290+
self.assertEqual(True, ipaddress.ip_network("10.0.0.0/8").is_private)
2291+
self.assertEqual(True, ipaddress.ip_network("127.0.0.0/8").is_private)
2292+
self.assertEqual(True, ipaddress.ip_network("169.254.0.0/16").is_private)
2293+
self.assertEqual(True, ipaddress.ip_network("172.16.0.0/12").is_private)
2294+
self.assertEqual(True, ipaddress.ip_network("192.0.0.0/29").is_private)
2295+
self.assertEqual(False, ipaddress.ip_network("192.0.0.9/32").is_private)
2296+
self.assertEqual(True, ipaddress.ip_network("192.0.0.170/31").is_private)
2297+
self.assertEqual(True, ipaddress.ip_network("192.0.2.0/24").is_private)
2298+
self.assertEqual(True, ipaddress.ip_network("192.168.0.0/16").is_private)
2299+
self.assertEqual(True, ipaddress.ip_network("198.18.0.0/15").is_private)
2300+
self.assertEqual(True, ipaddress.ip_network("198.51.100.0/24").is_private)
2301+
self.assertEqual(True, ipaddress.ip_network("203.0.113.0/24").is_private)
2302+
self.assertEqual(True, ipaddress.ip_network("240.0.0.0/4").is_private)
2303+
self.assertEqual(True, ipaddress.ip_network("255.255.255.255/32").is_private)
2304+
2305+
self.assertEqual(False, ipaddress.ip_network("::/0").is_private)
2306+
self.assertEqual(False, ipaddress.ip_network("::ff/128").is_private)
2307+
2308+
self.assertEqual(True, ipaddress.ip_network("::1/128").is_private)
2309+
self.assertEqual(True, ipaddress.ip_network("::/128").is_private)
2310+
self.assertEqual(True, ipaddress.ip_network("::ffff:0:0/96").is_private)
2311+
self.assertEqual(True, ipaddress.ip_network("100::/64").is_private)
2312+
self.assertEqual(True, ipaddress.ip_network("2001:2::/48").is_private)
2313+
self.assertEqual(False, ipaddress.ip_network("2001:3::/48").is_private)
2314+
self.assertEqual(True, ipaddress.ip_network("2001:db8::/32").is_private)
2315+
self.assertEqual(True, ipaddress.ip_network("2001:10::/28").is_private)
2316+
self.assertEqual(True, ipaddress.ip_network("fc00::/7").is_private)
2317+
self.assertEqual(True, ipaddress.ip_network("fe80::/10").is_private)
2318+
22812319
def testReservedIpv6(self):
22822320

22832321
self.assertEqual(True, ipaddress.ip_network('ffff::').is_multicast)
@@ -2351,6 +2389,20 @@ def testReservedIpv6(self):
23512389
self.assertEqual(True, ipaddress.ip_address('0::0').is_unspecified)
23522390
self.assertEqual(False, ipaddress.ip_address('::1').is_unspecified)
23532391

2392+
self.assertFalse(ipaddress.ip_address('64:ff9b:1::').is_global)
2393+
self.assertFalse(ipaddress.ip_address('2001::').is_global)
2394+
self.assertTrue(ipaddress.ip_address('2001:1::1').is_global)
2395+
self.assertTrue(ipaddress.ip_address('2001:1::2').is_global)
2396+
self.assertFalse(ipaddress.ip_address('2001:2::').is_global)
2397+
self.assertTrue(ipaddress.ip_address('2001:3::').is_global)
2398+
self.assertFalse(ipaddress.ip_address('2001:4::').is_global)
2399+
self.assertTrue(ipaddress.ip_address('2001:4:112::').is_global)
2400+
self.assertFalse(ipaddress.ip_address('2001:10::').is_global)
2401+
self.assertTrue(ipaddress.ip_address('2001:20::').is_global)
2402+
self.assertTrue(ipaddress.ip_address('2001:30::').is_global)
2403+
self.assertFalse(ipaddress.ip_address('2001:40::').is_global)
2404+
self.assertFalse(ipaddress.ip_address('2002::').is_global)
2405+
23542406
# some generic IETF reserved addresses
23552407
self.assertEqual(True, ipaddress.ip_address('100::').is_reserved)
23562408
self.assertEqual(True, ipaddress.ip_network('4000::1/128').is_reserved)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Fixed various false positives and false negatives in
2+
3+
* :attr:`ipaddress.IPv4Address.is_private` (see these docs for details)
4+
* :attr:`ipaddress.IPv4Address.is_global`
5+
* :attr:`ipaddress.IPv6Address.is_private`
6+
* :attr:`ipaddress.IPv6Address.is_global`
7+
8+
Also in the corresponding :class:`ipaddress.IPv4Network` and :class:`ipaddress.IPv6Network`
9+
attributes.

0 commit comments

Comments
 (0)