Skip to content

Commit 22adf29

Browse files
encukoujstasiak
andauthored
[3.9] gh-113171: gh-65056: Fix "private" (non-global) IP address ranges (GH-113179) (GH-113186) (GH-118177) (GH-118472)
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 7db40cd commit 22adf29

File tree

6 files changed

+195
-21
lines changed

6 files changed

+195
-21
lines changed

Doc/library/ipaddress.rst

Lines changed: 39 additions & 4 deletions
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.9.20
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.9.20
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/tools/susp-ignored.csv

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,14 @@ library/ipaddress,,:db00,2001:db00::0/24
169169
library/ipaddress,,::,2001:db00::0/24
170170
library/ipaddress,,:db00,2001:db00::0/ffff:ff00::
171171
library/ipaddress,,::,2001:db00::0/ffff:ff00::
172+
library/ipaddress,,:ff9b,64:ff9b:1::/48
173+
library/ipaddress,,::,64:ff9b:1::/48
174+
library/ipaddress,,::,2001::
175+
library/ipaddress,,::,2001:1::
176+
library/ipaddress,,::,2001:3::
177+
library/ipaddress,,::,2001:4:112::
178+
library/ipaddress,,::,2001:20::
179+
library/ipaddress,,::,2001:30::
172180
library/itertools,,:step,elements from seq[start:stop:step]
173181
library/itertools,,:stop,elements from seq[start:stop:step]
174182
library/itertools,,::,kernel = tuple(kernel)[::-1]

Doc/whatsnew/3.9.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1616,3 +1616,12 @@ tarfile
16161616
:exc:`DeprecationWarning`.
16171617
In Python 3.14, the default will switch to ``'data'``.
16181618
(Contributed by Petr Viktorin in :pep:`706`.)
1619+
1620+
Notable changes in 3.9.20
1621+
=========================
1622+
1623+
ipaddress
1624+
---------
1625+
1626+
* Fixed ``is_global`` and ``is_private`` behavior in ``IPv4Address``,
1627+
``IPv6Address``, ``IPv4Network`` and ``IPv6Network``.

Lib/ipaddress.py

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

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

13391362
@property
@@ -1537,13 +1560,15 @@ class _IPv4Constants:
15371560

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

1563+
# Not globally reachable address blocks listed on
1564+
# https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
15401565
_private_networks = [
15411566
IPv4Network('0.0.0.0/8'),
15421567
IPv4Network('10.0.0.0/8'),
15431568
IPv4Network('127.0.0.0/8'),
15441569
IPv4Network('169.254.0.0/16'),
15451570
IPv4Network('172.16.0.0/12'),
1546-
IPv4Network('192.0.0.0/29'),
1571+
IPv4Network('192.0.0.0/24'),
15471572
IPv4Network('192.0.0.170/31'),
15481573
IPv4Network('192.0.2.0/24'),
15491574
IPv4Network('192.168.0.0/16'),
@@ -1554,6 +1579,11 @@ class _IPv4Constants:
15541579
IPv4Network('255.255.255.255/32'),
15551580
]
15561581

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

15591589
_unspecified_address = IPv4Address('0.0.0.0')
@@ -1995,23 +2025,42 @@ def is_site_local(self):
19952025
@property
19962026
@functools.lru_cache()
19972027
def is_private(self):
1998-
"""Test if this address is allocated for private networks.
2028+
"""``True`` if the address is defined as not globally reachable by
2029+
iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
2030+
(for IPv6) with the following exceptions:
19992031
2000-
Returns:
2001-
A boolean, True if the address is reserved per
2002-
iana-ipv6-special-registry.
2032+
* ``is_private`` is ``False`` for ``100.64.0.0/10``
2033+
* For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
2034+
semantics of the underlying IPv4 addresses and the following condition holds
2035+
(see :attr:`IPv6Address.ipv4_mapped`)::
2036+
2037+
address.is_private == address.ipv4_mapped.is_private
20032038
2039+
``is_private`` has value opposite to :attr:`is_global`, except for the ``100.64.0.0/10``
2040+
IPv4 range where they are both ``False``.
20042041
"""
2005-
return any(self in net for net in self._constants._private_networks)
2042+
ipv4_mapped = self.ipv4_mapped
2043+
if ipv4_mapped is not None:
2044+
return ipv4_mapped.is_private
2045+
return (
2046+
any(self in net for net in self._constants._private_networks)
2047+
and all(self not in net for net in self._constants._private_networks_exceptions)
2048+
)
20062049

20072050
@property
20082051
def is_global(self):
2009-
"""Test if this address is allocated for public networks.
2052+
"""``True`` if the address is defined as globally reachable by
2053+
iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
2054+
(for IPv6) with the following exception:
20102055
2011-
Returns:
2012-
A boolean, true if the address is not reserved per
2013-
iana-ipv6-special-registry.
2056+
For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
2057+
semantics of the underlying IPv4 addresses and the following condition holds
2058+
(see :attr:`IPv6Address.ipv4_mapped`)::
2059+
2060+
address.is_global == address.ipv4_mapped.is_global
20142061
2062+
``is_global`` has value opposite to :attr:`is_private`, except for the ``100.64.0.0/10``
2063+
IPv4 range where they are both ``False``.
20152064
"""
20162065
return not self.is_private
20172066

@@ -2252,19 +2301,31 @@ class _IPv6Constants:
22522301

22532302
_multicast_network = IPv6Network('ff00::/8')
22542303

2304+
# Not globally reachable address blocks listed on
2305+
# https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
22552306
_private_networks = [
22562307
IPv6Network('::1/128'),
22572308
IPv6Network('::/128'),
22582309
IPv6Network('::ffff:0:0/96'),
2310+
IPv6Network('64:ff9b:1::/48'),
22592311
IPv6Network('100::/64'),
22602312
IPv6Network('2001::/23'),
2261-
IPv6Network('2001:2::/48'),
22622313
IPv6Network('2001:db8::/32'),
2263-
IPv6Network('2001:10::/28'),
2314+
# IANA says N/A, let's consider it not globally reachable to be safe
2315+
IPv6Network('2002::/16'),
22642316
IPv6Network('fc00::/7'),
22652317
IPv6Network('fe80::/10'),
22662318
]
22672319

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

Lib/test/test_ipaddress.py

Lines changed: 52 additions & 0 deletions
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)
Lines changed: 9 additions & 0 deletions
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)