Forbid enable ndp proxy when external netwrok has no IPv6 address scope

In neutron, user can create multiple ports with same IPv6 address if
the network has no IPv6 address scope. This maybe result in some
security issues.

This can be exploited by a malicious tenant via creating a subnet with
a prefix that covers an address that is already in use and take over
(part of) the traffic flowing towards that address. The success of the
attack depends on winning the race of who answers the NDP query first,
but still a 50% chance of capturing traffic seems dangerous. The attack
works not only against other addresses served by NDP proxy, but also
against other hosts that may exist, potentially even the gateway for
the external network.

So, we should use `IPv6 address scope` to ensure the IPv6 address is
unique when we want to use `ndp proxy` feature.

Depends-on: https://review.opendev.org/#/c/855997
Closes-Bug: #1987410
Change-Id: I0fa431a91a7679e409386a357a01c31ec5ad0cfd
This commit is contained in:
yangjianfeng 2022-09-05 11:41:55 +08:00
parent 12b21e235e
commit d600b3d433
4 changed files with 208 additions and 36 deletions

View File

@ -96,12 +96,130 @@ To configure NDP proxy, take the following steps:
User workflow
~~~~~~~~~~~~~
Assume the admin operator already prepared an IPv6 subnetpool:
``test-subnetpool``, its CIDR is 2001:db8::/96.
The basic steps to publish an IPv6 address to an external
network (such as: public network) are the following:
.. note::
In order to prevent potential
`security risk <https://bugs.launchpad.net/neutron/+bug/1987410>`_,
the `ndp proxy` feature require that use `address scope` to ensure the
uniqueness of the IPv6 address which published to external
#. Create an IPv6 address scope
.. code-block:: console
$ openstack address scope create test-ipv6-as --ip-version 6
+------------+--------------------------------------+
| Field | Value |
+------------+--------------------------------------+
| id | 24761ec5-b659-4358-b9ab-495ead15fa7a |
| ip_version | 6 |
| name | test-ipv6-as |
| project_id | bcb0c7a5338b4a46959e47971c58f0f1 |
| shared | False |
+------------+--------------------------------------+
#. Create an IPv6 subnet pool
.. code-block:: console
$ openstack subnet pool create test-subnetpool --address-scope test-ipv6-as \
--pool-prefix 2001:db8::/96 --default-prefix-length 112
+-------------------+--------------------------------------+
| Field | Value |
+-------------------+--------------------------------------+
| address_scope_id | 24761ec5-b659-4358-b9ab-495ead15fa7a |
| created_at | 2022-09-05T06:16:31Z |
| default_prefixlen | 112 |
| default_quota | None |
| description | |
| id | 4af07f59-45b8-424d-98c5-35d20ba61526 |
| ip_version | 6 |
| is_default | False |
| max_prefixlen | 128 |
| min_prefixlen | 64 |
| name | test-subnetpool |
| prefixes | 2001:db8::/96 |
| project_id | bcb0c7a5338b4a46959e47971c58f0f1 |
| revision_number | 0 |
| shared | False |
| tags | |
| updated_at | 2022-01-01T06:42:08Z |
+-------------------+--------------------------------------+
#. Create an external network
.. code-block:: console
$ openstack network create --external --provider-network-type flat \
--provider-physical-network public public
+---------------------------+--------------------------------------+
| Field | Value |
+---------------------------+--------------------------------------+
| admin_state_up | UP |
| availability_zone_hints | |
| availability_zones | |
| created_at | 2022-09-05T06:18:31Z |
| description | |
| dns_domain | None |
| id | 98b0f468-7be0-4530-919d-c4d9417c3abf |
| ipv4_address_scope | None |
| ipv6_address_scope | None |
| is_default | False |
| is_vlan_transparent | None |
| mtu | 1500 |
| name | public |
| port_security_enabled | True |
| project_id | bcb0c7a5338b4a46959e47971c58f0f1 |
| provider:network_type | flat |
| provider:physical_network | public |
| provider:segmentation_id | None |
| qos_policy_id | None |
| revision_number | 1 |
| router:external | External |
| segments | None |
| shared | False |
| status | ACTIVE |
| subnets | |
| tags | |
| updated_at | 2022-01-01T06:45:08Z |
+---------------------------+--------------------------------------+
#. Create an external subnet
.. code-block:: console
$ openstack subnet create --network public --subnet-pool test-subnetpool \
--prefix-length 112 --ip-version 6 --no-dhcp ext-sub
+----------------------+--------------------------------------+
| Field | Value |
+----------------------+--------------------------------------+
| allocation_pools | 2001:db8::2-2001:db8::ffff |
| cidr | 2001:db8::/112 |
| created_at | 2022-09-05T06:21:37Z |
| description | |
| dns_nameservers | |
| dns_publish_fixed_ip | None |
| enable_dhcp | False |
| gateway_ip | 2001:db8::1 |
| host_routes | |
| id | ec11de28-9b84-4cee-b6a1-0ed56135bcd8 |
| ip_version | 6 |
| ipv6_address_mode | None |
| ipv6_ra_mode | None |
| name | ext-sub |
| network_id | 98b0f468-7be0-4530-919d-c4d9417c3abf |
| project_id | bcb0c7a5338b4a46959e47971c58f0f1 |
| revision_number | 0 |
| segment_id | None |
| service_types | |
| subnetpool_id | 4af07f59-45b8-424d-98c5-35d20ba61526 |
| tags | |
| updated_at | 2022-01-01T06:47:08Z |
+----------------------+--------------------------------------+
#. Create a router:
.. code-block:: console
@ -200,14 +318,14 @@ network (such as: public network) are the following:
+----------------------+--------------------------------------+
| Field | Value |
+----------------------+--------------------------------------+
| allocation_pools | 2001:db8::2-2001:db8::ffff |
| cidr | 2001:db8::/112 |
| created_at | 2022-01-02T08:20:26Z |
| allocation_pools | 2001:db8::1:2-2001:db8::1:ffff |
| cidr | 2001:db8::1:0/112 |
| created_at | 2022-09-05T06:24:13Z |
| description | |
| dns_nameservers | |
| dns_publish_fixed_ip | None |
| enable_dhcp | True |
| gateway_ip | 2001:db8::1 |
| gateway_ip | 2001:db8::1:1 |
| host_routes | |
| id | 9bcf194c-d44f-4e6f-90da-98510ddef283 |
| ip_version | 6 |
@ -219,7 +337,7 @@ network (such as: public network) are the following:
| revision_number | 0 |
| segment_id | None |
| service_types | |
| subnetpool_id | 73c5311c-6750-43f5-9a69-b50c1c5694fd |
| subnetpool_id | 4af07f59-45b8-424d-98c5-35d20ba61526 |
| tags | |
| updated_at | 2022-01-02T08:20:26Z |
+----------------------+--------------------------------------+
@ -272,11 +390,11 @@ network (such as: public network) are the following:
.. code-block:: console
$ openstack port list --server test-server
+--------------------------------------+------+-------------------+------------------------------------------------------------------------------+--------+
| ID | Name | MAC Address | Fixed IP Addresses | Status |
+--------------------------------------+------+-------------------+------------------------------------------------------------------------------+--------+
| bdd64aa0-437a-4db6-bbca-99869426c908 | | fa:16:3e:ac:15:b8 | ip_address='2001:db8::284', subnet_id='9bcf194c-d44f-4e6f-90da-98510ddef283' | ACTIVE |
+--------------------------------------+------+-------------------+------------------------------------------------------------------------------+--------+
+--------------------------------------+------+-------------------+--------------------------------------------------------------------------------+--------+
| ID | Name | MAC Address | Fixed IP Addresses | Status |
+--------------------------------------+------+-------------------+--------------------------------------------------------------------------------+--------+
| bdd64aa0-437a-4db6-bbca-99869426c908 | | fa:16:3e:ac:15:b8 | ip_address='2001:db8::1:284', subnet_id='9bcf194c-d44f-4e6f-90da-98510ddef283' | ACTIVE |
+--------------------------------------+------+-------------------+--------------------------------------------------------------------------------+--------+
Create NDP proxy for the port
@ -289,7 +407,7 @@ network (such as: public network) are the following:
| created_at | 2022-01-02T08:25:31Z |
| description | |
| id | 73889fee-e322-443f-941e-142e4fc5f898 |
| ip_address | 2001:db8::284 |
| ip_address | 2001:db8::1:284 |
| name | test-np |
| port_id | bdd64aa0-437a-4db6-bbca-99869426c908 |
| project_id | bcb0c7a5338b4a46959e47971c58f0f1 |
@ -302,10 +420,10 @@ network (such as: public network) are the following:
.. code-block:: console
$ ping 2001:db8::284
PING 2001:db8::284(2001:db8::284) 56 data bytes
64 bytes from 2001:db8::284: icmp_seq=1 ttl=64 time=0.365 ms
64 bytes from 2001:db8::284: icmp_seq=2 ttl=64 time=0.385 ms
$ ping 2001:db8::1:284
PING 2001:db8::1:284(2001:db8::1:284) 56 data bytes
64 bytes from 2001:db8::1:284: icmp_seq=1 ttl=64 time=0.365 ms
64 bytes from 2001:db8::1:284: icmp_seq=2 ttl=64 time=0.385 ms
.. note::

View File

@ -75,11 +75,7 @@ class NDPProxyPlugin(l3_ndp_proxy.NDPProxyBase):
# parameter is always False.
enable_ndp_proxy = False
if result_dict.get(l3_apidef.EXTERNAL_GW_INFO, None):
# For already existed routers (created before this plugin
# enabled), they have no ndp_proxy_state object.
if not router_db.ndp_proxy_state:
enable_ndp_proxy = cfg.CONF.enable_ndp_proxy_by_default
else:
if router_db.ndp_proxy_state:
enable_ndp_proxy = router_db.ndp_proxy_state.enable_ndp_proxy
result_dict[l3_ext_ndp_proxy.ENABLE_NDP_PROXY] = enable_ndp_proxy
@ -148,6 +144,8 @@ class NDPProxyPlugin(l3_ndp_proxy.NDPProxyBase):
if not gw_port_id:
return False
port_dict = self.core_plugin.get_port(context.elevated(), gw_port_id)
if not self._check_ext_gw_network(context, port_dict['network_id']):
return False
v6_fixed_ips = [
fixed_ip for fixed_ip in port_dict['fixed_ips']
if (netaddr.IPNetwork(fixed_ip['ip_address']).version == V6)]
@ -158,6 +156,9 @@ class NDPProxyPlugin(l3_ndp_proxy.NDPProxyBase):
return False
def _check_ext_gw_network(self, context, network_id):
network = self.core_plugin.get_network(context, network_id)
if not network.get('ipv6_address_scope'):
return False
ext_subnets = self.core_plugin.get_subnets(
context.elevated(), filters={'network_id': network_id})
has_ipv6_subnet = False
@ -197,7 +198,7 @@ class NDPProxyPlugin(l3_ndp_proxy.NDPProxyBase):
if not ext_gw_support_ndp and ndp_proxy_state is True:
reason = _("The external network %s don't support "
"IPv6 ndp proxy, the network has no IPv6 "
"subnets.") % network_id
"subnets or has no IPv6 address scope") % network_id
raise exc.RouterGatewayNotValid(
router_id=router_db.id, reason=reason)
if ndp_proxy_state == lib_consts.ATTR_NOT_SPECIFIED:
@ -217,16 +218,20 @@ class NDPProxyPlugin(l3_ndp_proxy.NDPProxyBase):
ndp_proxy_state = request_body.get(
l3_ext_ndp_proxy.ENABLE_NDP_PROXY,
lib_consts.ATTR_NOT_SPECIFIED)
if ndp_proxy_state == lib_consts.ATTR_NOT_SPECIFIED:
return
if self._gateway_is_valid(context, router_db['gw_port_id']):
self._ensure_router_ndp_proxy_state_model(
context, router_db, ndp_proxy_state)
elif ndp_proxy_state:
gw_support_ndp = self._gateway_is_valid(
context, router_db['gw_port_id'])
if ndp_proxy_state is True and not gw_support_ndp:
reason = _("The router has no external gateway or the external "
"gateway port has no IPv6 address")
"gateway port has no IPv6 address or IPv6 address "
"scope")
raise exc.RouterGatewayNotValid(
router_id=router_db.id, reason=reason)
if ndp_proxy_state == lib_consts.ATTR_NOT_SPECIFIED:
self._ensure_router_ndp_proxy_state_model(
context, router_db, gw_support_ndp)
else:
self._ensure_router_ndp_proxy_state_model(
context, router_db, ndp_proxy_state)
@registry.receives(resources.ROUTER_INTERFACE, [events.BEFORE_DELETE])
def _check_router_remove_subnet_request(self, resource, event,

View File

@ -388,6 +388,9 @@ class L3NatTestCaseMixin(object):
data['router'][arg] = kwargs[arg]
if 'distributed' in kwargs:
data['router']['distributed'] = bool(kwargs['distributed'])
if 'enable_ndp_proxy' in kwargs:
data['router']['enable_ndp_proxy'] = \
bool(kwargs['enable_ndp_proxy'])
router_req = self.new_create_request('routers', data, fmt)
if set_context and tenant_id:
# create a specific auth context for this request

View File

@ -78,7 +78,16 @@ class L3NDPProxyTestCase(test_address_scope.AddressScopeTestCase,
ext_mgr=ext_mgr, service_plugins=svc_plugins, plugin=plugin)
self.ext_api = test_extensions.setup_extensions_middleware(ext_mgr)
self.ext_net = self._make_network(self.fmt, 'ext-net', True)
self.address_scope_id = self._make_address_scope(
self.fmt, constants.IP_VERSION_6,
**{'tenant_id': self.tenant_id})['address_scope']['id']
self.subnetpool_id = self._make_subnetpool(
self.fmt, ['2001::0/96'],
**{'address_scope_id': self.address_scope_id,
'default_prefixlen': 112, 'tenant_id': self.tenant_id,
'name': "test-ipv6-pool"})['subnetpool']['id']
self.ext_net = self._make_network(
self.fmt, 'ext-net', True)
self.ext_net_id = self.ext_net['network']['id']
self._set_net_external(self.ext_net_id)
self._ext_subnet_v4 = self._make_subnet(
@ -87,6 +96,7 @@ class L3NDPProxyTestCase(test_address_scope.AddressScopeTestCase,
self._ext_subnet_v4_id = self._ext_subnet_v4['subnet']['id']
self._ext_subnet_v6 = self._make_subnet(
self.fmt, self.ext_net, gateway="2001::1:1",
subnetpool_id=self.subnetpool_id,
cidr="2001::1:0/112",
ip_version=constants.IP_VERSION_6,
ipv6_ra_mode=constants.DHCPV6_STATEFUL,
@ -97,6 +107,7 @@ class L3NDPProxyTestCase(test_address_scope.AddressScopeTestCase,
self.private_net = self._make_network(self.fmt, 'private-net', True)
self.private_subnet = self._make_subnet(
self.fmt, self.private_net, gateway="2001::2:1",
subnetpool_id=self.subnetpool_id,
cidr="2001::2:0/112",
ip_version=constants.IP_VERSION_6,
ipv6_ra_mode=constants.DHCPV6_STATEFUL,
@ -249,11 +260,40 @@ class L3NDPProxyTestCase(test_address_scope.AddressScopeTestCase,
router_id = router['router']['id']
err_msg = ("Can not enable ndp proxy on router %s, The router has "
"no external gateway or the external gateway port has "
"no IPv6 address.") % router_id
"no IPv6 address or IPv6 address scope.") % router_id
self._update_router(router_id, {'enable_ndp_proxy': True},
expected_code=exc.HTTPConflict.code,
expected_message=err_msg)
def test_enable_ndp_proxy_without_address_scope(self):
with self.network() as ext_net, \
self.subnet(
cidr='2001::12:0/112',
ip_version=constants.IP_VERSION_6,
ipv6_ra_mode=constants.DHCPV6_STATEFUL,
ipv6_address_mode=constants.DHCPV6_STATEFUL):
self._set_net_external(ext_net['network']['id'])
res = self._make_router(
self.fmt, self.tenant_id,
external_gateway_info={'network_id': ext_net['network']['id']},
**{'enable_ndp_proxy': True})
expected_msg = (
"The external network %s don't support IPv6 ndp proxy, the "
"network has no IPv6 subnets or has no IPv6 address "
"scope.") % ext_net['network']['id']
self.assertTrue(expected_msg in res['NeutronError']['message'])
router = self._make_router(
self.fmt, self.tenant_id,
external_gateway_info={'network_id': ext_net['network']['id']})
expected_msg = (
"Can not enable ndp proxy on router %s, The router has no "
"external gateway or the external gateway port has no IPv6 "
"address or IPv6 address scope.") % router['router']['id']
self._update_router(
router['router']['id'], {'enable_ndp_proxy': True},
expected_code=exc.HTTPConflict.code,
expected_message=expected_msg)
def test_delete_router_gateway_with_enable_ndp_proxy(self):
with self.router() as router:
router_id = router['router']['id']
@ -281,6 +321,7 @@ class L3NDPProxyTestCase(test_address_scope.AddressScopeTestCase,
def test_create_ndp_proxy_with_invalid_port(self):
with self.subnet(
subnetpool_id=self.subnetpool_id,
cidr='2001::8:0/112',
ip_version=constants.IP_VERSION_6,
ipv6_ra_mode=constants.DHCPV6_STATEFUL,
@ -290,6 +331,7 @@ class L3NDPProxyTestCase(test_address_scope.AddressScopeTestCase,
ip_version=constants.IP_VERSION_6,
ipv6_ra_mode=constants.DHCPV6_STATEFUL,
ipv6_address_mode=constants.DHCPV6_STATEFUL,
subnetpool_id=self.subnetpool_id,
cidr='2001::9:0/112') as sub2, \
self.subnet(self.private_net) as sub3, \
self.port(sub1) as port1, \
@ -349,6 +391,7 @@ class L3NDPProxyTestCase(test_address_scope.AddressScopeTestCase,
def test_create_ndp_proxy_with_invalid_router(self):
with self.subnet(
subnetpool_id=self.subnetpool_id,
cidr='2001::8:0/112',
ipv6_ra_mode=constants.DHCPV6_STATEFUL,
ipv6_address_mode=constants.DHCPV6_STATEFUL,
@ -405,7 +448,8 @@ class L3NDPProxyTestCase(test_address_scope.AddressScopeTestCase,
with self.subnet(ip_version=constants.IP_VERSION_6,
ipv6_ra_mode=constants.DHCPV6_STATEFUL,
ipv6_address_mode=constants.DHCPV6_STATEFUL,
cidr='2001::50:1:0/112') as subnet, \
subnetpool_id=self.subnetpool_id,
cidr='2001::50:0/112') as subnet, \
self.port(subnet) as port:
subnet_id = subnet['subnet']['id']
port_id = port['port']['id']
@ -445,9 +489,10 @@ class L3NDPProxyTestCase(test_address_scope.AddressScopeTestCase,
port_id = port['port']['id']
self._router_interface_action(
'add', self.router1_id, subnet_id, None)
err_msg = ("The IPv6 address scope None of external network "
err_msg = ("The IPv6 address scope %s of external network "
"conflict with internal network's IPv6 address "
"scope %s.") % addr_scope['address_scope']['id']
"scope %s.") % (self.address_scope_id,
addr_scope['address_scope']['id'])
self._create_ndp_proxy(
self.router1_id, port_id,
expected_code=exc.HTTPConflict.code,
@ -496,6 +541,7 @@ class L3NDPProxyTestCase(test_address_scope.AddressScopeTestCase,
def test_enable_ndp_proxy_by_default_conf_option(self):
cfg.CONF.set_override("enable_ndp_proxy_by_default", True)
with self.subnet(
subnetpool_id=self.subnetpool_id,
cidr='2001::8:0/112',
ipv6_ra_mode=constants.DHCPV6_STATEFUL,
ipv6_address_mode=constants.DHCPV6_STATEFUL,