diff --git a/doc/source/admin/config-ndp-proxy.rst b/doc/source/admin/config-ndp-proxy.rst index cc7e9b60e1c..d62cb7354e3 100644 --- a/doc/source/admin/config-ndp-proxy.rst +++ b/doc/source/admin/config-ndp-proxy.rst @@ -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 `_, + 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:: diff --git a/neutron/services/ndp_proxy/plugin.py b/neutron/services/ndp_proxy/plugin.py index b73935cde83..1fbb8edbbfa 100644 --- a/neutron/services/ndp_proxy/plugin.py +++ b/neutron/services/ndp_proxy/plugin.py @@ -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, diff --git a/neutron/tests/unit/extensions/test_l3.py b/neutron/tests/unit/extensions/test_l3.py index c5c2758e08c..14039edf56a 100644 --- a/neutron/tests/unit/extensions/test_l3.py +++ b/neutron/tests/unit/extensions/test_l3.py @@ -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 diff --git a/neutron/tests/unit/extensions/test_l3_ndp_proxy.py b/neutron/tests/unit/extensions/test_l3_ndp_proxy.py index 7f8dd960c95..2814c6fee9f 100644 --- a/neutron/tests/unit/extensions/test_l3_ndp_proxy.py +++ b/neutron/tests/unit/extensions/test_l3_ndp_proxy.py @@ -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,