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)