From 96c1e57a7f8777b23b61e96ad19b07ff1791fdcf Mon Sep 17 00:00:00 2001 From: Adit Sarfaty Date: Thu, 14 Sep 2017 16:17:24 +0300 Subject: [PATCH] NSX|V3: Add DHCP relay firewall rules When FWaaS v1 or v2 are used, there is a need to add FW rules to allow the dhcp traffic to the relay server. Those rules are added to the firewall before the default deny rule. In case of FWaaS v2 - for each port separately. The admin utility handling a change in the DHCP relay configuration will now update the rules as well. Change-Id: I30e666085fe5cdf17d48984518c73f79bf8cdf55 --- .../plugins/nsx_v3/availability_zones.py | 7 +- vmware_nsx/plugins/nsx_v3/plugin.py | 75 ++++++++++++++++++- .../fwaas/nsx_v3/edge_fwaas_driver_base.py | 6 +- .../fwaas/nsx_v3/fwaas_callbacks_v1.py | 1 - .../admin/plugins/nsxv3/resources/routers.py | 48 +++++++----- .../admin/plugins/nsxv3/resources/utils.py | 37 +++++++++ .../tests/unit/nsx_v3/test_fwaas_v1_driver.py | 53 +++++++++++++ .../tests/unit/nsx_v3/test_fwaas_v2_driver.py | 63 +++++++++++++++- 8 files changed, 262 insertions(+), 28 deletions(-) diff --git a/vmware_nsx/plugins/nsx_v3/availability_zones.py b/vmware_nsx/plugins/nsx_v3/availability_zones.py index 021e3e2dfc..de0314c28c 100644 --- a/vmware_nsx/plugins/nsx_v3/availability_zones.py +++ b/vmware_nsx/plugins/nsx_v3/availability_zones.py @@ -181,7 +181,7 @@ class NsxV3AvailabilityZone(common_az.ConfiguredAvailabilityZone): nsxlib.feature_supported(nsxlib_consts.FEATURE_DHCP_RELAY)): relay_id = None if cfg.CONF.nsx_v3.init_objects_by_tags: - # Find the TZ by its tag + # Find the relay service by its tag relay_id = nsxlib.get_id_by_resource_and_tag( nsxlib.relay_service.resource_type, cfg.CONF.nsx_v3.search_objects_scope, @@ -191,8 +191,13 @@ class NsxV3AvailabilityZone(common_az.ConfiguredAvailabilityZone): relay_id = nsxlib.relay_service.get_id_by_name_or_id( self.dhcp_relay_service) self.dhcp_relay_service = relay_id + # if there is a relay service - also find the server ips + if self.dhcp_relay_service: + self.dhcp_relay_servers = nsxlib.relay_service.get_server_ips( + self.dhcp_relay_service) else: self.dhcp_relay_service = None + self.dhcp_relay_servers = None class NsxV3AvailabilityZones(common_az.ConfiguredAvailabilityZones): diff --git a/vmware_nsx/plugins/nsx_v3/plugin.py b/vmware_nsx/plugins/nsx_v3/plugin.py index 1d0412adae..88355de234 100644 --- a/vmware_nsx/plugins/nsx_v3/plugin.py +++ b/vmware_nsx/plugins/nsx_v3/plugin.py @@ -3305,6 +3305,26 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, return self.fwaas_callbacks.update_router_firewall( context, self.nsxlib, router_id, ports) + def _get_port_relay_servers(self, context, port_id, network_id=None): + if not network_id: + port = self.get_port(context, port_id) + network_id = port['network_id'] + net_az = self.get_network_az_by_net_id(context, network_id) + return net_az.dhcp_relay_servers + + def _get_port_relay_services(self): + # DHCP services: UDP 67, 68, 2535 + #TODO(asarfaty): use configurable ports + service1 = self.nsxlib.firewall_section.get_nsservice( + nsxlib_consts.L4_PORT_SET_NSSERVICE, + l4_protocol=nsxlib_consts.UDP, + destination_ports=['67-68']) + service2 = self.nsxlib.firewall_section.get_nsservice( + nsxlib_consts.L4_PORT_SET_NSSERVICE, + l4_protocol=nsxlib_consts.UDP, + destination_ports=['2535']) + return [service1, service2] + def get_extra_fw_rules(self, context, router_id, port_id=None): """Return firewall rules that should be added to the router firewall @@ -3317,8 +3337,59 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, port should be returned, and the rules should be ingress/egress (but not both) and include the source/dest nsx logical port. """ - #TODO(asarfaty): DHCP relay rules - return [] + extra_rules = [] + # DHCP relay rules: + # get the list of relevant relay servers + elv_ctx = context.elevated() + if port_id: + relay_servers = self._get_port_relay_servers(elv_ctx, port_id) + else: + relay_servers = [] + filters = {'device_owner': [l3_db.DEVICE_OWNER_ROUTER_INTF], + 'device_id': [router_id]} + ports = self.get_ports(elv_ctx, filters=filters) + for port in ports: + port_relay_servers = self._get_port_relay_servers( + elv_ctx, port['id'], network_id=port['network_id']) + if port_relay_servers: + relay_servers.extend(port_relay_servers) + + # Add rules to allow dhcp traffic relay servers + if relay_servers: + # if it is a single port, the source/dest is this logical port + if port_id: + _net_id, nsx_port_id = nsx_db.get_nsx_switch_and_port_id( + context.session, port_id) + port_target = [{'target_type': 'LogicalPort', + 'target_id': nsx_port_id}] + else: + port_target = None + # translate the relay server ips to the firewall format + relay_target = [] + if self.fwaas_callbacks: + relay_target = (self.fwaas_callbacks.fwaas_driver. + translate_addresses_to_target(set(relay_servers))) + + dhcp_services = self._get_port_relay_services() + + # ingress rule + extra_rules.append({ + 'display_name': "DHCP Relay ingress traffic", + 'action': nsxlib_consts.FW_ACTION_ALLOW, + 'sources': relay_target, + 'destinations': port_target, + 'services': dhcp_services, + 'direction': 'IN'}) + # egress rule + extra_rules.append({ + 'display_name': "DHCP Relay egress traffic", + 'action': nsxlib_consts.FW_ACTION_ALLOW, + 'destinations': relay_target, + 'sources': port_target, + 'services': dhcp_services, + 'direction': 'OUT'}) + + return extra_rules def _get_ports_and_address_groups(self, context, router_id, network_id, exclude_sub_ids=None): diff --git a/vmware_nsx/services/fwaas/nsx_v3/edge_fwaas_driver_base.py b/vmware_nsx/services/fwaas/nsx_v3/edge_fwaas_driver_base.py index b4d32ffc82..dd9392cf92 100644 --- a/vmware_nsx/services/fwaas/nsx_v3/edge_fwaas_driver_base.py +++ b/vmware_nsx/services/fwaas/nsx_v3/edge_fwaas_driver_base.py @@ -98,7 +98,7 @@ class CommonEdgeFwaasV3Driver(fwaas_base.FwaasDriverBase): cidr, consts.IPV6 if netaddr.valid_ipv6(cidr) else consts.IPV4) - def _translate_addresses(self, cidrs): + def translate_addresses_to_target(self, cidrs): return [self._translate_cidr(ip) for ip in cidrs] @staticmethod @@ -170,7 +170,7 @@ class CommonEdgeFwaasV3Driver(fwaas_base.FwaasDriverBase): 'target_id': replace_dest}] nsx_rule['direction'] = 'IN' elif rule.get('destination_ip_address'): - nsx_rule['destinations'] = self._translate_addresses( + nsx_rule['destinations'] = self.translate_addresses_to_target( [rule['destination_ip_address']]) if replace_src: # set this value as the source logical port, @@ -179,7 +179,7 @@ class CommonEdgeFwaasV3Driver(fwaas_base.FwaasDriverBase): 'target_id': replace_src}] nsx_rule['direction'] = 'OUT' elif rule.get('source_ip_address'): - nsx_rule['sources'] = self._translate_addresses( + nsx_rule['sources'] = self.translate_addresses_to_target( [rule['source_ip_address']]) if rule.get('protocol'): nsx_rule['services'] = self._translate_services(rule) diff --git a/vmware_nsx/services/fwaas/nsx_v3/fwaas_callbacks_v1.py b/vmware_nsx/services/fwaas/nsx_v3/fwaas_callbacks_v1.py index 07dab6580e..44c2af9e8c 100644 --- a/vmware_nsx/services/fwaas/nsx_v3/fwaas_callbacks_v1.py +++ b/vmware_nsx/services/fwaas/nsx_v3/fwaas_callbacks_v1.py @@ -53,7 +53,6 @@ class Nsxv3FwaasCallbacksV1(com_clbcks.NsxFwaasCallbacks): This method should be called on FWaaS updates, and on router interfaces changes. """ - # find the backend router and its firewall section nsx_id, sect_id = self.fwaas_driver.get_backend_router_and_fw_section( context, router_id) diff --git a/vmware_nsx/shell/admin/plugins/nsxv3/resources/routers.py b/vmware_nsx/shell/admin/plugins/nsxv3/resources/routers.py index 8df8ffadf4..350f92eb92 100644 --- a/vmware_nsx/shell/admin/plugins/nsxv3/resources/routers.py +++ b/vmware_nsx/shell/admin/plugins/nsxv3/resources/routers.py @@ -188,27 +188,37 @@ def update_dhcp_relay(resource, event, trigger, **kwargs): # initialize the availability zones and nsxlib config.register_nsxv3_azs(cfg.CONF, cfg.CONF.nsx_v3.availability_zones) - # get all neutron router interfaces ports admin_cxt = neutron_context.get_admin_context() with utils.NsxV3PluginWrapper() as plugin: - filters = {'device_owner': [l3_db.DEVICE_OWNER_ROUTER_INTF]} - ports = plugin.get_ports(admin_cxt, filters=filters) - for port in ports: - # get the backend router port by the tag - nsx_port_id = nsxlib.get_id_by_resource_and_tag( - 'LogicalRouterDownLinkPort', - 'os-neutron-rport-id', port['id']) - if not nsx_port_id: - LOG.warning("Couldn't find nsx router port for interface %s", - port['id']) - continue - # get the network of this port - network_id = port['network_id'] - # check the relay service on the az of the network - az = plugin.get_network_az_by_net_id(admin_cxt, network_id) - nsxlib.logical_router_port.update( - nsx_port_id, relay_service_uuid=az.dhcp_relay_service) - #TODO(asarfaty) also update the firewall rules of the routers + # Make sure FWaaS was initialized + plugin.init_fwaas_for_admin_utils() + + # get all neutron routers and interfaces ports + routers = plugin.get_routers(admin_cxt) + for router in routers: + LOG.info("Updating router %s", router['id']) + filters = {'device_owner': [l3_db.DEVICE_OWNER_ROUTER_INTF], + 'device_id': [router['id']]} + ports = plugin.get_ports(admin_cxt, filters=filters) + for port in ports: + # get the backend router port by the tag + nsx_port_id = nsxlib.get_id_by_resource_and_tag( + 'LogicalRouterDownLinkPort', + 'os-neutron-rport-id', port['id']) + if not nsx_port_id: + LOG.warning("Couldn't find nsx router port for interface " + "%s", port['id']) + continue + # get the network of this port + network_id = port['network_id'] + # check the relay service on the az of the network + az = plugin.get_network_az_by_net_id(admin_cxt, network_id) + nsxlib.logical_router_port.update( + nsx_port_id, relay_service_uuid=az.dhcp_relay_service) + + # if FWaaS is enables, also update the firewall rules + plugin.update_router_firewall(admin_cxt, router['id']) + LOG.info("Done.") diff --git a/vmware_nsx/shell/admin/plugins/nsxv3/resources/utils.py b/vmware_nsx/shell/admin/plugins/nsxv3/resources/utils.py index 253a419527..6a8cd77d27 100644 --- a/vmware_nsx/shell/admin/plugins/nsxv3/resources/utils.py +++ b/vmware_nsx/shell/admin/plugins/nsxv3/resources/utils.py @@ -13,14 +13,22 @@ # under the License. +from oslo_config import cfg + from neutron.db import db_base_plugin_v2 +from neutron import manager from neutron_lib import context from neutron_lib.plugins import constants as const from neutron_lib.plugins import directory +from neutron_fwaas.services.firewall import fwaas_plugin as fwaas_plugin_v1 +from neutron_fwaas.services.firewall import fwaas_plugin_v2 + from vmware_nsx.db import db as nsx_db from vmware_nsx.plugins.nsx_v3 import plugin from vmware_nsx.plugins.nsx_v3 import utils as v3_utils +from vmware_nsx.services.fwaas.nsx_v3 import fwaas_callbacks_v1 +from vmware_nsx.services.fwaas.nsx_v3 import fwaas_callbacks_v2 from vmware_nsxlib.v3 import nsx_constants _NSXLIB = None @@ -107,6 +115,35 @@ class NsxV3PluginWrapper(plugin.NsxV3Plugin): def __exit__(self, exc_type, exc_value, traceback): directory.add_plugin(const.CORE, None) + def _init_fwaas_plugin(self, provider, callbacks_class, plugin_callbacks): + fwaas_plugin_class = manager.NeutronManager.load_class_for_provider( + 'neutron.service_plugins', provider) + fwaas_plugin = fwaas_plugin_class() + self.fwaas_callbacks = callbacks_class(self.nsxlib) + # override the fwplugin_rpc since there is no RPC support in adminutils + self.fwaas_callbacks.fwplugin_rpc = plugin_callbacks(fwaas_plugin) + + def init_fwaas_for_admin_utils(self): + # initialize the FWaaS plugin and callbacks + self.fwaas_callbacks = None + # This is an ugly patch to find out if it is v1 or v2 + service_plugins = cfg.CONF.service_plugins + for srv_plugin in service_plugins: + if 'firewall' in srv_plugin: + if 'v2' in srv_plugin: + # FWaaS V2 + self._init_fwaas_plugin( + 'firewall_v2', + fwaas_callbacks_v2.Nsxv3FwaasCallbacksV2, + fwaas_plugin_v2.FirewallCallbacks) + else: + # FWaaS V1 + self._init_fwaas_plugin( + 'firewall', + fwaas_callbacks_v1.Nsxv3FwaasCallbacksV1, + fwaas_plugin_v1.FirewallCallbacks) + return + def _init_dhcp_metadata(self): pass diff --git a/vmware_nsx/tests/unit/nsx_v3/test_fwaas_v1_driver.py b/vmware_nsx/tests/unit/nsx_v3/test_fwaas_v1_driver.py index 1e0e1e90cf..8f6d472101 100644 --- a/vmware_nsx/tests/unit/nsx_v3/test_fwaas_v1_driver.py +++ b/vmware_nsx/tests/unit/nsx_v3/test_fwaas_v1_driver.py @@ -29,6 +29,9 @@ from vmware_nsxlib.v3 import nsx_constants as consts FAKE_FW_ID = 'fake_fw_uuid' FAKE_ROUTER_ID = 'fake_rtr_uuid' MOCK_NSX_ID = 'nsx_router_id' +FAKE_PORT_ID = 'fake_port_uuid' +FAKE_NET_ID = 'fake_net_uuid' +FAKE_NSX_PORT_ID = 'fake_nsx_port_uuid' MOCK_DEFAULT_RULE_ID = 'nsx_default_rule_id' MOCK_SECTION_ID = 'sec_id' DEFAULT_RULE = {'is_default': True, @@ -177,6 +180,8 @@ class Nsxv3FwaasTestCase(test_v3_plugin.NsxV3PluginTestCaseMixin): "update") as update_fw, \ mock.patch.object(self.plugin, '_get_router_interfaces', return_value=[]), \ + mock.patch.object(self.plugin, 'get_ports', + return_value=[]), \ mock.patch.object(self.plugin, 'get_router', return_value=apply_list[0]), \ mock.patch.object(self.plugin.fwaas_callbacks, @@ -199,6 +204,8 @@ class Nsxv3FwaasTestCase(test_v3_plugin.NsxV3PluginTestCaseMixin): "update") as update_fw,\ mock.patch.object(self.plugin, '_get_router_interfaces', return_value=[]), \ + mock.patch.object(self.plugin, 'get_ports', + return_value=[]), \ mock.patch.object(self.plugin, 'get_router', return_value=apply_list[0]), \ mock.patch.object(self.plugin.fwaas_callbacks, @@ -269,6 +276,8 @@ class Nsxv3FwaasTestCase(test_v3_plugin.NsxV3PluginTestCaseMixin): "update") as update_fw, \ mock.patch.object(self.plugin, '_get_router_interfaces', return_value=[]), \ + mock.patch.object(self.plugin, 'get_ports', + return_value=[]), \ mock.patch.object(self.plugin, 'get_router', return_value=apply_list[0]), \ mock.patch.object(self.plugin.fwaas_callbacks, @@ -281,3 +290,47 @@ class Nsxv3FwaasTestCase(test_v3_plugin.NsxV3PluginTestCaseMixin): update_fw.assert_called_once_with( MOCK_SECTION_ID, rules=[self._default_rule()]) + + def test_create_firewall_with_dhcp_relay(self): + apply_list = self._fake_apply_list() + firewall = self._fake_firewall_no_rule() + relay_server = '1.1.1.1' + port = {'id': FAKE_PORT_ID, 'network_id': FAKE_NET_ID} + with mock.patch("vmware_nsxlib.v3.security.NsxLibFirewallSection." + "update") as update_fw,\ + mock.patch.object(self.plugin, '_get_router_interfaces', + return_value=[port]), \ + mock.patch.object(self.plugin, 'get_ports', + return_value=[port]), \ + mock.patch.object(self.plugin, 'get_router', + return_value=apply_list[0]), \ + mock.patch.object(self.plugin, '_get_port_relay_servers', + return_value=[relay_server]),\ + mock.patch.object(self.plugin.fwaas_callbacks, + '_get_router_firewall_id', + return_value=firewall['id']), \ + mock.patch.object(self.plugin.fwaas_callbacks, + '_get_fw_from_plugin', + return_value=firewall): + self.firewall.create_firewall('nsx', apply_list, firewall) + # expecting 2 allow rules for the relay servers + default rule + expected_rules = expected_rules = [ + {'display_name': "DHCP Relay ingress traffic", + 'action': consts.FW_ACTION_ALLOW, + 'destinations': None, + 'sources': [{'target_id': relay_server, + 'target_type': 'IPv4Address'}], + 'services': self.plugin._get_port_relay_services(), + 'direction': 'IN'}, + {'display_name': "DHCP Relay egress traffic", + 'action': consts.FW_ACTION_ALLOW, + 'sources': None, + 'destinations': [{'target_id': relay_server, + 'target_type': 'IPv4Address'}], + 'services': self.plugin._get_port_relay_services(), + 'direction': 'OUT'}, + self._default_rule() + ] + update_fw.assert_called_once_with( + MOCK_SECTION_ID, + rules=expected_rules) diff --git a/vmware_nsx/tests/unit/nsx_v3/test_fwaas_v2_driver.py b/vmware_nsx/tests/unit/nsx_v3/test_fwaas_v2_driver.py index 42ffc5c4bd..942c95f97e 100644 --- a/vmware_nsx/tests/unit/nsx_v3/test_fwaas_v2_driver.py +++ b/vmware_nsx/tests/unit/nsx_v3/test_fwaas_v2_driver.py @@ -29,6 +29,7 @@ from vmware_nsxlib.v3 import nsx_constants as consts FAKE_FW_ID = 'fake_fw_uuid' FAKE_ROUTER_ID = 'fake_rtr_uuid' FAKE_PORT_ID = 'fake_port_uuid' +FAKE_NET_ID = 'fake_net_uuid' FAKE_NSX_PORT_ID = 'fake_nsx_port_uuid' MOCK_NSX_ID = 'nsx_nsx_router_id' MOCK_DEFAULT_RULE_ID = 'nsx_default_rule_id' @@ -191,9 +192,11 @@ class Nsxv3FwaasTestCase(test_v3_plugin.NsxV3PluginTestCaseMixin): def test_create_firewall_no_rules(self): apply_list = self._fake_apply_list() firewall = self._fake_empty_firewall_group() - port = {'id': FAKE_PORT_ID} + port = {'id': FAKE_PORT_ID, 'network_id': FAKE_NET_ID} with mock.patch.object(self.plugin, '_get_router_interfaces', return_value=[port]),\ + mock.patch.object(self.plugin, 'get_port', + return_value=port),\ mock.patch.object(self.plugin.fwaas_callbacks, 'get_port_fwg', return_value=firewall),\ mock.patch("vmware_nsx.db.db.get_nsx_switch_and_port_id", @@ -224,9 +227,11 @@ class Nsxv3FwaasTestCase(test_v3_plugin.NsxV3PluginTestCaseMixin): apply_list = self._fake_apply_list() rule_list = self._fake_rules_v4(is_ingress=is_ingress) firewall = self._fake_firewall_group(rule_list, is_ingress=is_ingress) - port = {'id': FAKE_PORT_ID} + port = {'id': FAKE_PORT_ID, 'network_id': FAKE_NET_ID} with mock.patch.object(self.plugin, '_get_router_interfaces', return_value=[port]),\ + mock.patch.object(self.plugin, 'get_port', + return_value=port),\ mock.patch.object(self.plugin.fwaas_callbacks, 'get_port_fwg', return_value=firewall),\ mock.patch("vmware_nsx.db.db.get_nsx_switch_and_port_id", @@ -302,3 +307,57 @@ class Nsxv3FwaasTestCase(test_v3_plugin.NsxV3PluginTestCaseMixin): update_fw.assert_called_once_with( MOCK_SECTION_ID, rules=[self._default_rule()]) + + def test_create_firewall_with_dhcp_relay(self): + apply_list = self._fake_apply_list() + firewall = self._fake_empty_firewall_group() + port = {'id': FAKE_PORT_ID, 'network_id': FAKE_NET_ID} + relay_server = '1.1.1.1' + with mock.patch.object(self.plugin, '_get_router_interfaces', + return_value=[port]),\ + mock.patch.object(self.plugin, 'get_port', + return_value=port),\ + mock.patch.object(self.plugin, '_get_port_relay_servers', + return_value=[relay_server]),\ + mock.patch.object(self.plugin.fwaas_callbacks, 'get_port_fwg', + return_value=firewall),\ + mock.patch("vmware_nsx.db.db.get_nsx_switch_and_port_id", + return_value=(0, FAKE_NSX_PORT_ID)),\ + mock.patch("vmware_nsxlib.v3.security.NsxLibFirewallSection." + "update") as update_fw: + self.firewall.create_firewall_group('nsx', apply_list, firewall) + # expecting 2 allow rules for the relay servers, + # 2 block rules for the logical port (egress & ingress) + # and last default allow all rule + expected_rules = [ + {'display_name': "DHCP Relay ingress traffic", + 'action': consts.FW_ACTION_ALLOW, + 'destinations': [{'target_type': 'LogicalPort', + 'target_id': FAKE_NSX_PORT_ID}], + 'sources': [{'target_id': relay_server, + 'target_type': 'IPv4Address'}], + 'services': self.plugin._get_port_relay_services(), + 'direction': 'IN'}, + {'display_name': "DHCP Relay egress traffic", + 'action': consts.FW_ACTION_ALLOW, + 'sources': [{'target_type': 'LogicalPort', + 'target_id': FAKE_NSX_PORT_ID}], + 'destinations': [{'target_id': relay_server, + 'target_type': 'IPv4Address'}], + 'services': self.plugin._get_port_relay_services(), + 'direction': 'OUT'}, + {'display_name': "Block port ingress", + 'action': consts.FW_ACTION_DROP, + 'destinations': [{'target_type': 'LogicalPort', + 'target_id': FAKE_NSX_PORT_ID}], + 'direction': 'IN'}, + {'display_name': "Block port egress", + 'action': consts.FW_ACTION_DROP, + 'sources': [{'target_type': 'LogicalPort', + 'target_id': FAKE_NSX_PORT_ID}], + 'direction': 'OUT'}, + self._default_rule() + ] + update_fw.assert_called_once_with( + MOCK_SECTION_ID, + rules=expected_rules)