Skip to content

Commit

Permalink
pythonGH-113171: Fix "private" (really non-global) IP address ranges
Browse files Browse the repository at this point in the history
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
  • Loading branch information
jstasiak committed Dec 15, 2023
1 parent f34e22c commit b83b987
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 5 deletions.
89 changes: 85 additions & 4 deletions Lib/ipaddress.py
Expand Up @@ -12,6 +12,7 @@


import functools
from itertools import combinations

IPV4LENGTH = 32
IPV6LENGTH = 128
Expand Down Expand Up @@ -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')

Expand All @@ -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'),
Expand Down Expand Up @@ -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'),
]
Expand Down
19 changes: 18 additions & 1 deletion Lib/test/test_ipaddress.py
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit b83b987

Please sign in to comment.