Skip to content

Commit f86b17a

Browse files
encukoujstasiak
andauthored
[3.12] gh-113171: gh-65056: Fix "private" (non-global) IP address ranges (GH-113179) (GH-113186) (GH-118177)
* GH-113171: Fix "private" (non-global) IP address ranges (GH-113179) 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 * GH-65056: Improve the IP address' is_global/is_private documentation (GH-113186) It wasn't clear what the semantics of is_global/is_private are and, when one gets to the bottom of it, it's not quite so simple (hence the exceptions listed). (cherry picked from commit 2a4cbf1) (cherry picked from commit 40d75c2) --------- Co-authored-by: Jakub Stasiak <jakub@stasiak.at>
1 parent 2eaf9ba commit f86b17a

File tree

5 files changed

+157
-24
lines changed

5 files changed

+157
-24
lines changed

Doc/library/ipaddress.rst

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

179179
.. attribute:: is_private
180180

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

185207
.. attribute:: is_global
186208

187-
``True`` if the address is allocated for public networks. See
209+
``True`` if the address is defined as globally reachable by
188210
iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
189-
(for IPv6).
211+
(for IPv6) with the following exception:
212+
213+
For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
214+
semantics of the underlying IPv4 addresses and the following condition holds
215+
(see :attr:`IPv6Address.ipv4_mapped`)::
216+
217+
address.is_global == address.ipv4_mapped.is_global
218+
219+
``is_global`` has value opposite to :attr:`is_private`, except for the shared address space
220+
(``100.64.0.0/10`` range) where they are both ``False``.
190221

191222
.. versionadded:: 3.4
192223

224+
.. versionchanged:: 3.12.4
225+
226+
Fixed some false positives and false negatives, see :attr:`is_private` for details.
227+
193228
.. attribute:: is_unspecified
194229

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

Doc/whatsnew/3.12.rst

+9
Original file line numberDiff line numberDiff line change
@@ -2457,3 +2457,12 @@ Removed
24572457

24582458
* Remove the ``PyUnicode_InternImmortal()`` function macro.
24592459
(Contributed by Victor Stinner in :gh:`85858`.)
2460+
2461+
Notable changes in 3.12.4
2462+
=========================
2463+
2464+
ipaddress
2465+
---------
2466+
2467+
* Fixed ``is_global`` and ``is_private`` behavior in ``IPv4Address``,
2468+
``IPv6Address``, ``IPv4Network`` and ``IPv6Network``.

Lib/ipaddress.py

+80-19
Original file line numberDiff line numberDiff line change
@@ -1086,7 +1086,11 @@ def is_private(self):
10861086
"""
10871087
return any(self.network_address in priv_network and
10881088
self.broadcast_address in priv_network
1089-
for priv_network in self._constants._private_networks)
1089+
for priv_network in self._constants._private_networks) and all(
1090+
self.network_address not in network and
1091+
self.broadcast_address not in network
1092+
for network in self._constants._private_networks_exceptions
1093+
)
10901094

10911095
@property
10921096
def is_global(self):
@@ -1333,18 +1337,41 @@ def is_reserved(self):
13331337
@property
13341338
@functools.lru_cache()
13351339
def is_private(self):
1336-
"""Test if this address is allocated for private networks.
1340+
"""``True`` if the address is defined as not globally reachable by
1341+
iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
1342+
(for IPv6) with the following exceptions:
13371343
1338-
Returns:
1339-
A boolean, True if the address is reserved per
1340-
iana-ipv4-special-registry.
1344+
* ``is_private`` is ``False`` for ``100.64.0.0/10``
1345+
* For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
1346+
semantics of the underlying IPv4 addresses and the following condition holds
1347+
(see :attr:`IPv6Address.ipv4_mapped`)::
13411348
1349+
address.is_private == address.ipv4_mapped.is_private
1350+
1351+
``is_private`` has value opposite to :attr:`is_global`, except for the ``100.64.0.0/10``
1352+
IPv4 range where they are both ``False``.
13421353
"""
1343-
return any(self in net for net in self._constants._private_networks)
1354+
return (
1355+
any(self in net for net in self._constants._private_networks)
1356+
and all(self not in net for net in self._constants._private_networks_exceptions)
1357+
)
13441358

13451359
@property
13461360
@functools.lru_cache()
13471361
def is_global(self):
1362+
"""``True`` if the address is defined as globally reachable by
1363+
iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
1364+
(for IPv6) with the following exception:
1365+
1366+
For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
1367+
semantics of the underlying IPv4 addresses and the following condition holds
1368+
(see :attr:`IPv6Address.ipv4_mapped`)::
1369+
1370+
address.is_global == address.ipv4_mapped.is_global
1371+
1372+
``is_global`` has value opposite to :attr:`is_private`, except for the ``100.64.0.0/10``
1373+
IPv4 range where they are both ``False``.
1374+
"""
13481375
return self not in self._constants._public_network and not self.is_private
13491376

13501377
@property
@@ -1548,13 +1575,15 @@ class _IPv4Constants:
15481575

15491576
_public_network = IPv4Network('100.64.0.0/10')
15501577

1578+
# Not globally reachable address blocks listed on
1579+
# https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
15511580
_private_networks = [
15521581
IPv4Network('0.0.0.0/8'),
15531582
IPv4Network('10.0.0.0/8'),
15541583
IPv4Network('127.0.0.0/8'),
15551584
IPv4Network('169.254.0.0/16'),
15561585
IPv4Network('172.16.0.0/12'),
1557-
IPv4Network('192.0.0.0/29'),
1586+
IPv4Network('192.0.0.0/24'),
15581587
IPv4Network('192.0.0.170/31'),
15591588
IPv4Network('192.0.2.0/24'),
15601589
IPv4Network('192.168.0.0/16'),
@@ -1565,6 +1594,11 @@ class _IPv4Constants:
15651594
IPv4Network('255.255.255.255/32'),
15661595
]
15671596

1597+
_private_networks_exceptions = [
1598+
IPv4Network('192.0.0.9/32'),
1599+
IPv4Network('192.0.0.10/32'),
1600+
]
1601+
15681602
_reserved_network = IPv4Network('240.0.0.0/4')
15691603

15701604
_unspecified_address = IPv4Address('0.0.0.0')
@@ -2007,27 +2041,42 @@ def is_site_local(self):
20072041
@property
20082042
@functools.lru_cache()
20092043
def is_private(self):
2010-
"""Test if this address is allocated for private networks.
2044+
"""``True`` if the address is defined as not globally reachable by
2045+
iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
2046+
(for IPv6) with the following exceptions:
20112047
2012-
Returns:
2013-
A boolean, True if the address is reserved per
2014-
iana-ipv6-special-registry, or is ipv4_mapped and is
2015-
reserved in the iana-ipv4-special-registry.
2048+
* ``is_private`` is ``False`` for ``100.64.0.0/10``
2049+
* For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
2050+
semantics of the underlying IPv4 addresses and the following condition holds
2051+
(see :attr:`IPv6Address.ipv4_mapped`)::
20162052
2053+
address.is_private == address.ipv4_mapped.is_private
2054+
2055+
``is_private`` has value opposite to :attr:`is_global`, except for the ``100.64.0.0/10``
2056+
IPv4 range where they are both ``False``.
20172057
"""
20182058
ipv4_mapped = self.ipv4_mapped
20192059
if ipv4_mapped is not None:
20202060
return ipv4_mapped.is_private
2021-
return any(self in net for net in self._constants._private_networks)
2061+
return (
2062+
any(self in net for net in self._constants._private_networks)
2063+
and all(self not in net for net in self._constants._private_networks_exceptions)
2064+
)
20222065

20232066
@property
20242067
def is_global(self):
2025-
"""Test if this address is allocated for public networks.
2068+
"""``True`` if the address is defined as globally reachable by
2069+
iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
2070+
(for IPv6) with the following exception:
20262071
2027-
Returns:
2028-
A boolean, true if the address is not reserved per
2029-
iana-ipv6-special-registry.
2072+
For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
2073+
semantics of the underlying IPv4 addresses and the following condition holds
2074+
(see :attr:`IPv6Address.ipv4_mapped`)::
2075+
2076+
address.is_global == address.ipv4_mapped.is_global
20302077
2078+
``is_global`` has value opposite to :attr:`is_private`, except for the ``100.64.0.0/10``
2079+
IPv4 range where they are both ``False``.
20312080
"""
20322081
return not self.is_private
20332082

@@ -2268,19 +2317,31 @@ class _IPv6Constants:
22682317

22692318
_multicast_network = IPv6Network('ff00::/8')
22702319

2320+
# Not globally reachable address blocks listed on
2321+
# https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
22712322
_private_networks = [
22722323
IPv6Network('::1/128'),
22732324
IPv6Network('::/128'),
22742325
IPv6Network('::ffff:0:0/96'),
2326+
IPv6Network('64:ff9b:1::/48'),
22752327
IPv6Network('100::/64'),
22762328
IPv6Network('2001::/23'),
2277-
IPv6Network('2001:2::/48'),
22782329
IPv6Network('2001:db8::/32'),
2279-
IPv6Network('2001:10::/28'),
2330+
# IANA says N/A, let's consider it not globally reachable to be safe
2331+
IPv6Network('2002::/16'),
22802332
IPv6Network('fc00::/7'),
22812333
IPv6Network('fe80::/10'),
22822334
]
22832335

2336+
_private_networks_exceptions = [
2337+
IPv6Network('2001:1::1/128'),
2338+
IPv6Network('2001:1::2/128'),
2339+
IPv6Network('2001:3::/32'),
2340+
IPv6Network('2001:4:112::/48'),
2341+
IPv6Network('2001:20::/28'),
2342+
IPv6Network('2001:30::/28'),
2343+
]
2344+
22842345
_reserved_networks = [
22852346
IPv6Network('::/8'), IPv6Network('100::/8'),
22862347
IPv6Network('200::/7'), IPv6Network('400::/6'),

Lib/test/test_ipaddress.py

+20-1
Original file line numberDiff line numberDiff line change
@@ -2269,6 +2269,10 @@ def testReservedIpv4(self):
22692269
self.assertEqual(True, ipaddress.ip_address(
22702270
'172.31.255.255').is_private)
22712271
self.assertEqual(False, ipaddress.ip_address('172.32.0.0').is_private)
2272+
self.assertFalse(ipaddress.ip_address('192.0.0.0').is_global)
2273+
self.assertTrue(ipaddress.ip_address('192.0.0.9').is_global)
2274+
self.assertTrue(ipaddress.ip_address('192.0.0.10').is_global)
2275+
self.assertFalse(ipaddress.ip_address('192.0.0.255').is_global)
22722276

22732277
self.assertEqual(True,
22742278
ipaddress.ip_address('169.254.100.200').is_link_local)
@@ -2294,6 +2298,7 @@ def testPrivateNetworks(self):
22942298
self.assertEqual(True, ipaddress.ip_network("169.254.0.0/16").is_private)
22952299
self.assertEqual(True, ipaddress.ip_network("172.16.0.0/12").is_private)
22962300
self.assertEqual(True, ipaddress.ip_network("192.0.0.0/29").is_private)
2301+
self.assertEqual(False, ipaddress.ip_network("192.0.0.9/32").is_private)
22972302
self.assertEqual(True, ipaddress.ip_network("192.0.0.170/31").is_private)
22982303
self.assertEqual(True, ipaddress.ip_network("192.0.2.0/24").is_private)
22992304
self.assertEqual(True, ipaddress.ip_network("192.168.0.0/16").is_private)
@@ -2310,8 +2315,8 @@ def testPrivateNetworks(self):
23102315
self.assertEqual(True, ipaddress.ip_network("::/128").is_private)
23112316
self.assertEqual(True, ipaddress.ip_network("::ffff:0:0/96").is_private)
23122317
self.assertEqual(True, ipaddress.ip_network("100::/64").is_private)
2313-
self.assertEqual(True, ipaddress.ip_network("2001::/23").is_private)
23142318
self.assertEqual(True, ipaddress.ip_network("2001:2::/48").is_private)
2319+
self.assertEqual(False, ipaddress.ip_network("2001:3::/48").is_private)
23152320
self.assertEqual(True, ipaddress.ip_network("2001:db8::/32").is_private)
23162321
self.assertEqual(True, ipaddress.ip_network("2001:10::/28").is_private)
23172322
self.assertEqual(True, ipaddress.ip_network("fc00::/7").is_private)
@@ -2390,6 +2395,20 @@ def testReservedIpv6(self):
23902395
self.assertEqual(True, ipaddress.ip_address('0::0').is_unspecified)
23912396
self.assertEqual(False, ipaddress.ip_address('::1').is_unspecified)
23922397

2398+
self.assertFalse(ipaddress.ip_address('64:ff9b:1::').is_global)
2399+
self.assertFalse(ipaddress.ip_address('2001::').is_global)
2400+
self.assertTrue(ipaddress.ip_address('2001:1::1').is_global)
2401+
self.assertTrue(ipaddress.ip_address('2001:1::2').is_global)
2402+
self.assertFalse(ipaddress.ip_address('2001:2::').is_global)
2403+
self.assertTrue(ipaddress.ip_address('2001:3::').is_global)
2404+
self.assertFalse(ipaddress.ip_address('2001:4::').is_global)
2405+
self.assertTrue(ipaddress.ip_address('2001:4:112::').is_global)
2406+
self.assertFalse(ipaddress.ip_address('2001:10::').is_global)
2407+
self.assertTrue(ipaddress.ip_address('2001:20::').is_global)
2408+
self.assertTrue(ipaddress.ip_address('2001:30::').is_global)
2409+
self.assertFalse(ipaddress.ip_address('2001:40::').is_global)
2410+
self.assertFalse(ipaddress.ip_address('2002::').is_global)
2411+
23932412
# some generic IETF reserved addresses
23942413
self.assertEqual(True, ipaddress.ip_address('100::').is_reserved)
23952414
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)