Skip to content

Commit b83b987

Browse files
committed
pythonGH-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] python#61602
1 parent f34e22c commit b83b987

File tree

2 files changed

+103
-5
lines changed

2 files changed

+103
-5
lines changed

Lib/ipaddress.py

Lines changed: 85 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313

1414
import functools
15+
from itertools import combinations
1516

1617
IPV4LENGTH = 32
1718
IPV6LENGTH = 128
@@ -1549,6 +1550,65 @@ def is_global(self):
15491550
not self.is_private)
15501551

15511552

1553+
def _address_exclude_many(network, others):
1554+
"""
1555+
A version of IPv4Network/IPv6Network address_exclude() but for multiple networks
1556+
to exclude in the same call.
1557+
1558+
The following are programming errors and will raise an exception:
1559+
1560+
* Network type mismatch (IPv4 vs IPv6)
1561+
* Networks in `others` overlapping each other
1562+
* Networks in `others` not ovarlapping the provided `network`
1563+
1564+
Returns:
1565+
A list of networks left after excluding `others` from `network`.
1566+
"""
1567+
# Precondition checks
1568+
for o in others:
1569+
if not network.overlaps(o):
1570+
raise AssertionError(f"No overlap between {network} and {o}")
1571+
1572+
for a, b in combinations(others, 2):
1573+
if a.overlaps(b):
1574+
raise AssertionError(f"{a} overlaps {b}")
1575+
1576+
networks = [network]
1577+
1578+
for o in others:
1579+
networks = [
1580+
result_network
1581+
for input_network in networks
1582+
for result_network in (
1583+
input_network.address_exclude(o)
1584+
if input_network.overlaps(o)
1585+
else [input_network]
1586+
)
1587+
]
1588+
1589+
# Integrity checks to make sure we haven't done something really wrong
1590+
addresses_started_with = network.num_addresses
1591+
addresses_excluded = sum(o.num_addresses for o in others)
1592+
addresses_left = sum(n.num_addresses for n in networks)
1593+
expected_addresses_left = addresses_started_with - addresses_excluded
1594+
1595+
if addresses_left != expected_addresses_left:
1596+
raise AssertionError(
1597+
f"Should have {expected_addresses_left} addresses left, got {addresses_left}"
1598+
)
1599+
1600+
for n in networks:
1601+
for o in others:
1602+
if n.overlaps(o):
1603+
raise AssertionError(f"{n} overlaps {o}")
1604+
1605+
for a, b in combinations(networks, 2):
1606+
if a.overlaps(b):
1607+
raise AssertionError(f'{a} overlaps {b}')
1608+
1609+
return networks
1610+
1611+
15521612
class _IPv4Constants:
15531613
_linklocal_network = IPv4Network('169.254.0.0/16')
15541614

@@ -1558,13 +1618,21 @@ class _IPv4Constants:
15581618

15591619
_public_network = IPv4Network('100.64.0.0/10')
15601620

1621+
# Not globally reachable address blocks listed on
1622+
# https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
15611623
_private_networks = [
15621624
IPv4Network('0.0.0.0/8'),
15631625
IPv4Network('10.0.0.0/8'),
15641626
IPv4Network('127.0.0.0/8'),
15651627
IPv4Network('169.254.0.0/16'),
15661628
IPv4Network('172.16.0.0/12'),
1567-
IPv4Network('192.0.0.0/29'),
1629+
*_address_exclude_many(
1630+
IPv4Network('192.0.0.0/24'),
1631+
[
1632+
IPv4Network('192.0.0.9/32'),
1633+
IPv4Network('192.0.0.10/32'),
1634+
],
1635+
),
15681636
IPv4Network('192.0.0.170/31'),
15691637
IPv4Network('192.0.2.0/24'),
15701638
IPv4Network('192.168.0.0/16'),
@@ -2310,15 +2378,28 @@ class _IPv6Constants:
23102378

23112379
_multicast_network = IPv6Network('ff00::/8')
23122380

2381+
# Not globally reachable address blocks listed on
2382+
# https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
23132383
_private_networks = [
23142384
IPv6Network('::1/128'),
23152385
IPv6Network('::/128'),
23162386
IPv6Network('::ffff:0:0/96'),
2387+
IPv6Network('64:ff9b:1::/48'),
23172388
IPv6Network('100::/64'),
2318-
IPv6Network('2001::/23'),
2319-
IPv6Network('2001:2::/48'),
2389+
*_address_exclude_many(
2390+
IPv6Network('2001::/23'),
2391+
[
2392+
IPv6Network('2001:1::1/128'),
2393+
IPv6Network('2001:1::2/128'),
2394+
IPv6Network('2001:3::/32'),
2395+
IPv6Network('2001:4:112::/48'),
2396+
IPv6Network('2001:20::/28'),
2397+
IPv6Network('2001:30::/28'),
2398+
],
2399+
),
23202400
IPv6Network('2001:db8::/32'),
2321-
IPv6Network('2001:10::/28'),
2401+
# IANA says N/A, let's consider it not globally reachable to be safe
2402+
IPv6Network('2002::/16'),
23222403
IPv6Network('fc00::/7'),
23232404
IPv6Network('fe80::/10'),
23242405
]

Lib/test/test_ipaddress.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2288,6 +2288,10 @@ def testReservedIpv4(self):
22882288
self.assertEqual(True, ipaddress.ip_address(
22892289
'172.31.255.255').is_private)
22902290
self.assertEqual(False, ipaddress.ip_address('172.32.0.0').is_private)
2291+
self.assertFalse(ipaddress.ip_address('192.0.0.0').is_global)
2292+
self.assertTrue(ipaddress.ip_address('192.0.0.9').is_global)
2293+
self.assertTrue(ipaddress.ip_address('192.0.0.10').is_global)
2294+
self.assertFalse(ipaddress.ip_address('192.0.0.255').is_global)
22912295

22922296
self.assertEqual(True,
22932297
ipaddress.ip_address('169.254.100.200').is_link_local)
@@ -2329,7 +2333,6 @@ def testPrivateNetworks(self):
23292333
self.assertEqual(True, ipaddress.ip_network("::/128").is_private)
23302334
self.assertEqual(True, ipaddress.ip_network("::ffff:0:0/96").is_private)
23312335
self.assertEqual(True, ipaddress.ip_network("100::/64").is_private)
2332-
self.assertEqual(True, ipaddress.ip_network("2001::/23").is_private)
23332336
self.assertEqual(True, ipaddress.ip_network("2001:2::/48").is_private)
23342337
self.assertEqual(True, ipaddress.ip_network("2001:db8::/32").is_private)
23352338
self.assertEqual(True, ipaddress.ip_network("2001:10::/28").is_private)
@@ -2409,6 +2412,20 @@ def testReservedIpv6(self):
24092412
self.assertEqual(True, ipaddress.ip_address('0::0').is_unspecified)
24102413
self.assertEqual(False, ipaddress.ip_address('::1').is_unspecified)
24112414

2415+
self.assertFalse(ipaddress.ip_address('64:ff9b:1::').is_global)
2416+
self.assertFalse(ipaddress.ip_address('2001::').is_global)
2417+
self.assertTrue(ipaddress.ip_address('2001:1::1').is_global)
2418+
self.assertTrue(ipaddress.ip_address('2001:1::2').is_global)
2419+
self.assertFalse(ipaddress.ip_address('2001:2::').is_global)
2420+
self.assertTrue(ipaddress.ip_address('2001:3::').is_global)
2421+
self.assertFalse(ipaddress.ip_address('2001:4::').is_global)
2422+
self.assertTrue(ipaddress.ip_address('2001:4:112::').is_global)
2423+
self.assertFalse(ipaddress.ip_address('2001:10::').is_global)
2424+
self.assertTrue(ipaddress.ip_address('2001:20::').is_global)
2425+
self.assertTrue(ipaddress.ip_address('2001:30::').is_global)
2426+
self.assertFalse(ipaddress.ip_address('2001:40::').is_global)
2427+
self.assertFalse(ipaddress.ip_address('2002::').is_global)
2428+
24122429
# some generic IETF reserved addresses
24132430
self.assertEqual(True, ipaddress.ip_address('100::').is_reserved)
24142431
self.assertEqual(True, ipaddress.ip_network('4000::1/128').is_reserved)

0 commit comments

Comments
 (0)