10000 GH-113171: Fix "private" (non-global) IP address ranges by jstasiak · Pull Request #113179 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

GH-113171: Fix "private" (non-global) IP address ranges #113179

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
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] #61602
  • Loading branch information
jstasiak committed Dec 15, 2023
commit b83b9874a5bcfe6eb3233849371fb35ccf2fa60f
89 changes: 85 additions & 4 deletions Lib/ipaddress.py
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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