From b83b9874a5bcfe6eb3233849371fb35ccf2fa60f Mon Sep 17 00:00:00 2001 From: Jakub Stasiak Date: Fri, 15 Dec 2023 14:47:19 +0100 Subject: [PATCH] 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. 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] https://github.com/python/cpython/issues/61602 --- Lib/ipaddress.py | 89 ++++++++++++++++++++++++++++++++++++-- Lib/test/test_ipaddress.py | 19 +++++++- 2 files changed, 103 insertions(+), 5 deletions(-) diff --git a/Lib/ipaddress.py b/Lib/ipaddress.py index e398cc138308d9..395173464e591f 100644 --- a/Lib/ipaddress.py +++ b/Lib/ipaddress.py @@ -12,6 +12,7 @@ import functools +from itertools import combinations IPV4LENGTH = 32 IPV6LENGTH = 128 @@ -1549,6 +1550,65 @@ def is_global(self): not self.is_private) +def _address_exclude_many(network, others): + """ + A version of IPv4Network/IPv6Network address_exclude() but for multiple networks + to exclude in the same call. + + The following are programming errors and will raise an exception: + + * Network type mismatch (IPv4 vs IPv6) + * Networks in `others` overlapping each other + * Networks in `others` not ovarlapping the provided `network` + + Returns: + A list of networks left after excluding `others` from `network`. + """ + # Precondition checks + for o in others: + if not network.overlaps(o): + raise AssertionError(f"No overlap between {network} and {o}") + + for a, b in combinations(others, 2): + if a.overlaps(b): + raise AssertionError(f"{a} overlaps {b}") + + networks = [network] + + for o in others: + networks = [ + result_network + for input_network in networks + for result_network in ( + input_network.address_exclude(o) + if input_network.overlaps(o) + else [input_network] + ) + ] + + # Integrity checks to make sure we haven't done something really wrong + addresses_started_with = network.num_addresses + addresses_excluded = sum(o.num_addresses for o in others) + addresses_left = sum(n.num_addresses for n in networks) + expected_addresses_left = addresses_started_with - addresses_excluded + + if addresses_left != expected_addresses_left: + raise AssertionError( + f"Should have {expected_addresses_left} addresses left, got {addresses_left}" + ) + + for n in networks: + for o in others: + if n.overlaps(o): + raise AssertionError(f"{n} overlaps {o}") + + for a, b in combinations(networks, 2): + if a.overlaps(b): + raise AssertionError(f'{a} overlaps {b}') + + return networks + + class _IPv4Constants: _linklocal_network = IPv4Network('169.254.0.0/16') @@ -1558,13 +1618,21 @@ 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'), + *_address_exclude_many( + IPv4Network('192.0.0.0/24'), + [ + IPv4Network('192.0.0.9/32'), + IPv4Network('192.0.0.10/32'), + ], + ), IPv4Network('192.0.0.170/31'), IPv4Network('192.0.2.0/24'), IPv4Network('192.168.0.0/16'), @@ -2310,15 +2378,28 @@ 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'), + *_address_exclude_many( + IPv6Network('2001::/23'), + [ + 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'), + ], + ), 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'), ] diff --git a/Lib/test/test_ipaddress.py b/Lib/test/test_ipaddress.py index b4952acc2b61b1..41488a84975e17 100644 --- a/Lib/test/test_ipaddress.py +++ b/Lib/test/test_ipaddress.py @@ -2288,6 +2288,10 @@ def testReservedIpv4(self): 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) @@ -2329,7 +2333,6 @@ def testPrivateNetworks(self): 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(True, ipaddress.ip_network("2001:db8::/32").is_private) self.assertEqual(True, ipaddress.ip_network("2001:10::/28").is_private) @@ -2409,6 +2412,20 @@ def testReservedIpv6(self): 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)