Merge "Change API to validate network MTU minimums"

This commit is contained in:
Zuul 2023-05-04 20:47:52 +00:00 committed by Gerrit Code Review
commit fd17662611
9 changed files with 205 additions and 5 deletions

View File

@ -195,8 +195,8 @@ Project network considerations
Dataplane
---------
Both the Linux bridge and the Open vSwitch dataplane modules support
forwarding IPv6
All dataplane modules, including OVN, Open vSwitch and Linux bridge,
support forwarding IPv6
packets amongst the guests and router ports. Similar to IPv4, there is no
special configuration or setup required to enable the dataplane to properly
forward packets from the source to the destination using IPv6. Note that these
@ -204,6 +204,15 @@ dataplanes will forward Link-local Address (LLA) packets between hosts on the
same network just fine without any participation or setup by OpenStack
components after the ports are all connected and MAC addresses learned.
.. warning::
The only exception to this is the setting of the MTU value on
the network an IPv6 subnet is created on. If the MTU is less than
1280 octets (the minimum link MTU value specified in
`RFC 8200 <https://www.rfc-editor.org/rfc/rfc8200>`__), then it
could lead to issues configuring both IPv6 and IPv4 addresses on
the network, leaving the subnets unusable. For that reason, the API
validates the MTU value when subnets are created to avoid this issue.
Addresses for subnets
---------------------

View File

@ -130,6 +130,13 @@ IPv6. IPv6 uses RA via the L3 agent because the DHCP agent only supports
IPv4. Instances using IPv4 and IPv6 should obtain the same MTU value
regardless of method.
.. note::
If you are using an MTU value on your network below 1280, please
read the warning listed in the
`IPv6 configuration guide <./config-ipv6.html#project-network-considerations>`__
before creating any subnets.
Networks with enabled vlan transparency
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -38,6 +38,13 @@ NAT for IPv4 network traffic and directly routes IPv6 network traffic.
| status | ACTIVE |
+-------------------------+--------------+
.. note::
If you are using an MTU value on your network below 1280, please
read the warning listed in the
`IPv6 configuration guide <../config-ipv6.html#project-network-considerations>`__
before creating any subnets.
#. Create a IPv4 subnet on the self-service network.
.. code-block:: console

View File

@ -89,3 +89,6 @@ LOWEST_AGENT_BINDING_INDEX = 1
# Neutron-lib defines this with a /64 but it should be /128
METADATA_V6_CIDR = constants.METADATA_V6_IP + '/128'
# TODO(haleyb): move this constant to neutron_lib.constants
IPV4_MIN_MTU = 68

View File

@ -58,6 +58,7 @@ from neutron.db import ipam_pluggable_backend
from neutron.db import models_v2
from neutron.db import rbac_db_mixin as rbac_mixin
from neutron.db import standardattrdescription_db as stattr_db
from neutron.exceptions import mtu as mtu_exc
from neutron.extensions import subnetpool_prefix_ops
from neutron import ipam
from neutron.ipam import exceptions as ipam_exc
@ -466,6 +467,10 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
# context.
getattr(network, 'rbac_entries')
# validate 'mtu' parameter
if 'mtu' in n:
self._validate_change_network_mtu(context, id, n['mtu'])
# The filter call removes attributes from the body received from
# the API that are logically tied to network resources but are
# stored in other database tables handled by extensions
@ -473,6 +478,28 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
ndb_utils.filter_non_model_columns(n, models_v2.Network))
return self._make_network_dict(network, context=context)
def _validate_change_network_mtu(self, context, id, mtu):
# can support either ip_version
if mtu >= constants.IPV6_MIN_MTU:
return
subnets = self._get_subnets_by_network(context, id)
if len(subnets) == 0:
return
# at least one subnet present, if below IPv4 minimum we fail early
if mtu < _constants.IPV4_MIN_MTU:
raise mtu_exc.NetworkMTUSubnetConflict(
net_id=id, mtu=_constants.IPV4_MIN_MTU)
# We do not need to check IPv4 subnets as they will have been
# caught by above IPV4_MIN_MTU check
for subnet in subnets:
if (subnet.ip_version == constants.IP_VERSION_6 and
mtu < constants.IPV6_MIN_MTU):
raise mtu_exc.NetworkMTUSubnetConflict(
net_id=id, mtu=constants.IPV6_MIN_MTU)
def _ensure_network_not_in_use(self, context, net_id):
non_auto_ports = context.session.query(
models_v2.Port.id).filter_by(network_id=net_id).filter(
@ -715,6 +742,23 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
"Prefix Delegation.")
raise exc.BadRequest(resource='subnets', msg=reason)
def _validate_subnet_network_mtu(self, network, subnet):
"""Validates that network mtu is correct for subnet association"""
mtu = network.mtu
if not mtu or mtu >= constants.IPV6_MIN_MTU:
return
# if below IPv4 minimum we fail early
if mtu < _constants.IPV4_MIN_MTU:
raise mtu_exc.NetworkMTUSubnetConflict(net_id=network.id, mtu=mtu)
# We do not need to check IPv4 subnets as they will have been
# caught by above IPV4_MIN_MTU check
ip_version = subnet.get('ip_version')
if (ip_version == constants.IP_VERSION_6 and
mtu < constants.IPV6_MIN_MTU):
raise mtu_exc.NetworkMTUSubnetConflict(net_id=network.id, mtu=mtu)
def _update_router_gw_ports(self, context, network, subnet):
l3plugin = directory.get_plugin(plugin_constants.L3)
if l3plugin:
@ -876,6 +920,7 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
with db_api.CONTEXT_WRITER.using(context):
network = self._get_network(context,
subnet['subnet']['network_id'])
self._validate_subnet_network_mtu(network, s)
subnet, ipam_subnet = self.ipam.allocate_subnet(context,
network,
subnet['subnet'],

28
neutron/exceptions/mtu.py Normal file
View File

@ -0,0 +1,28 @@
# Copyright (c) 2023 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from neutron_lib import exceptions as e
from neutron._i18n import _
# TODO(haleyb): Move to n-lib
class NetworkMTUSubnetConflict(e.Conflict):
"""A conflict error due to MTU being invalid on said network.
:param net_id: The UUID of the network
:param mtu: The minimum MTU required by a subnet for the network
"""
message = _("MTU of %(net_id)s is not valid, subnet requires a "
"minimum of %(mtu)s")

View File

@ -49,6 +49,7 @@ import neutron
from neutron.api import api_common
from neutron.api import extensions
from neutron.api.v2 import router
from neutron.common import _constants as common_constants
from neutron.common import ipv6_utils
from neutron.common.ovn import utils as ovn_utils
from neutron.common import test_lib
@ -60,6 +61,7 @@ from neutron.db import ipam_backend_mixin
from neutron.db.models import l3 as l3_models
from neutron.db.models import securitygroup as sg_models
from neutron.db import models_v2
from neutron.exceptions import mtu as mtu_exc
from neutron.ipam.drivers.neutrondb_ipam import driver as ipam_driver
from neutron.ipam import exceptions as ipam_exc
from neutron.objects import network as network_obj
@ -373,7 +375,7 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase):
'admin_state_up': admin_state_up,
'tenant_id': tenant_id}}
for arg in (('admin_state_up', 'tenant_id', 'shared',
'vlan_transparent',
'vlan_transparent', 'mtu',
'availability_zone_hints') + (arg_list or ())):
# Arg must be present
if arg in kwargs:
@ -7066,7 +7068,8 @@ class NeutronDbPluginV2AsMixinTestCase(NeutronDbPluginV2TestCase,
super(NeutronDbPluginV2AsMixinTestCase, self).setUp()
self.plugin = importutils.import_object(DB_PLUGIN_KLASS)
self.context = context.get_admin_context()
self.net_data = {'network': {'id': 'fake-id',
self.net_id = uuidutils.generate_uuid()
self.net_data = {'network': {'id': self.net_id,
'name': 'net1',
'admin_state_up': True,
'tenant_id': TEST_TENANT_ID,
@ -7075,7 +7078,7 @@ class NeutronDbPluginV2AsMixinTestCase(NeutronDbPluginV2TestCase,
def test_create_network_with_default_status(self):
net = self.plugin.create_network(self.context, self.net_data)
default_net_create_status = 'ACTIVE'
expected = [('id', 'fake-id'), ('name', 'net1'),
expected = [('id', self.net_id), ('name', 'net1'),
('admin_state_up', True), ('tenant_id', TEST_TENANT_ID),
('shared', False), ('status', default_net_create_status)]
for k, v in expected:
@ -7113,6 +7116,81 @@ class NeutronDbPluginV2AsMixinTestCase(NeutronDbPluginV2TestCase,
new_subnetpool_id,
None)
def test_create_subnet_invalid_network_mtu_ipv4_returns_409(self):
self.net_data['network']['mtu'] = common_constants.IPV4_MIN_MTU - 1
net = self.plugin.create_network(self.context, self.net_data)
self._create_subnet(self.fmt,
net['id'],
'10.0.0.0/24',
webob.exc.HTTPConflict.code)
def test_create_subnet_invalid_network_mtu_ipv6_returns_409(self):
self.net_data['network']['mtu'] = constants.IPV6_MIN_MTU - 1
net = self.plugin.create_network(self.context, self.net_data)
self._create_subnet(self.fmt,
net['id'],
'2001:db8:0:1::/64',
webob.exc.HTTPConflict.code,
ip_version=constants.IP_VERSION_6)
def test_update_network_invalid_mtu(self):
self.net_data['network']['mtu'] = 1500
net = self.plugin.create_network(self.context, self.net_data)
# This should succeed with no subnets
self.net_data['network']['mtu'] = common_constants.IPV4_MIN_MTU - 1
self.plugin.update_network(self.context, net['id'], self.net_data)
# reset mtu
self.net_data['network']['mtu'] = 1500
self.plugin.update_network(self.context, net['id'], self.net_data)
self._create_subnet(self.fmt,
net['id'],
'10.0.0.0/24',
ip_version=constants.IP_VERSION_4)
# These should succeed with just an IPv4 subnet present
self.net_data['network']['mtu'] = constants.IPV6_MIN_MTU
self.plugin.update_network(self.context, net['id'], self.net_data)
self.net_data['network']['mtu'] = constants.IPV6_MIN_MTU - 1
self.plugin.update_network(self.context, net['id'], self.net_data)
self.net_data['network']['mtu'] = common_constants.IPV4_MIN_MTU
self.plugin.update_network(self.context, net['id'], self.net_data)
# This should fail with any subnets present
self.net_data['network']['mtu'] = common_constants.IPV4_MIN_MTU - 1
with testlib_api.ExpectedException(mtu_exc.NetworkMTUSubnetConflict):
self.plugin.update_network(self.context, net['id'], self.net_data)
def test_update_network_invalid_mtu_ipv4_ipv6(self):
self.net_data['network']['mtu'] = 1500
net = self.plugin.create_network(self.context, self.net_data)
self._create_subnet(self.fmt,
net['id'],
'10.0.0.0/24',
ip_version=constants.IP_VERSION_4)
self._create_subnet(self.fmt,
net['id'],
'2001:db8:0:1::/64',
ip_version=constants.IP_VERSION_6)
# This should succeed with both subnets present
self.net_data['network']['mtu'] = constants.IPV6_MIN_MTU
self.plugin.update_network(self.context, net['id'], self.net_data)
# These should all fail with both subnets present
with testlib_api.ExpectedException(mtu_exc.NetworkMTUSubnetConflict):
self.net_data['network']['mtu'] = constants.IPV6_MIN_MTU - 1
self.plugin.update_network(self.context, net['id'], self.net_data)
with testlib_api.ExpectedException(mtu_exc.NetworkMTUSubnetConflict):
self.net_data['network']['mtu'] = common_constants.IPV4_MIN_MTU
self.plugin.update_network(self.context, net['id'], self.net_data)
with testlib_api.ExpectedException(mtu_exc.NetworkMTUSubnetConflict):
self.net_data['network']['mtu'] = common_constants.IPV4_MIN_MTU - 1
self.plugin.update_network(self.context, net['id'], self.net_data)
class TestNetworks(testlib_api.SqlTestCase):
def setUp(self):

View File

@ -322,6 +322,7 @@ class FakeNetwork(object):
'availability_zone_hints': [],
'is_default': False,
'standard_attr_id': 1,
'mtu': 1500,
}
# Overwrite default attributes.

View File

@ -0,0 +1,22 @@
---
fixes:
- |
The Neutron API has been changed to validate network MTU minimums.
A network's MTU is now only valid if it is the minimum value
allowed based on the IP version of the associated subnets,
68 for IPv4 and 1280 for IPv6.
This minimum is now enforced in the following ways:
* When a subnet is associated with a network, validate
the MTU is large enough for the IP version. Not only
would the subnet be unusable if it was allowed, but the
Linux kernel can fail adding addresses and configuring
network settings like the MTU.
* When a network MTU is changed, validate the MTU is large
enough for any currently associated subnets. Allowing a
smaller MTU would render any existing subnets unusable.
See bug `1988069 <https://bugs.launchpad.net/neutron/+bug/1988069>`_
for more information.