# Copyright 2019 VMware, Inc. # All Rights Reserved # # 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. import random import netaddr from oslo_config import cfg from oslo_log import log as logging from neutron_lib.exceptions import firewall_v2 as exceptions from vmware_nsx.extensions import projectpluginmap from vmware_nsx.services.fwaas.common import fwaas_callbacks_v2 as \ com_callbacks from vmware_nsx.services.fwaas.common import v3_utils from vmware_nsxlib.v3 import exceptions as nsx_lib_exc from vmware_nsxlib.v3 import nsx_constants from vmware_nsxlib.v3.policy import constants as policy_constants from vmware_nsxlib.v3 import utils as nsxlib_utils LOG = logging.getLogger(__name__) GATEWAY_POLICY_NAME = 'Tier1 %s gateway policy' DEFAULT_RULE_NAME = 'Default LR Layer3 Rule' DEFAULT_RULE_ID = 'default_rule' RULE_NAME_PREFIX = 'Fwaas-' ROUTER_FW_TAG = 'os-router-firewall' class NsxpFwaasCallbacksV2(com_callbacks.NsxCommonv3FwaasCallbacksV2): """NSX-P RPC callbacks for Firewall As A Service V2.""" def __init__(self, with_rpc): super(NsxpFwaasCallbacksV2, self).__init__(with_rpc) self.internal_driver = None if self.fwaas_enabled: self.internal_driver = self.fwaas_driver @property def plugin_type(self): return projectpluginmap.NsxPlugins.NSX_P @property def nsxpolicy(self): return self.core_plugin.nsxpolicy def _get_default_backend_rule(self, router_id): """Return the default allow-all rule entry This rule entry will be added to the end of the rules list """ return self.nsxpolicy.gateway_policy.build_entry( DEFAULT_RULE_NAME, policy_constants.DEFAULT_DOMAIN, router_id, self._get_random_rule_id(DEFAULT_RULE_ID), description=DEFAULT_RULE_NAME, sequence_number=None, action=nsx_constants.FW_ACTION_ALLOW, scope=[self.nsxpolicy.tier1.get_path(router_id)], source_groups=None, dest_groups=None, direction=nsx_constants.IN_OUT) def _translate_service(self, project_id, router_id, rule): """Return the NSX Policy service id matching the FW rule service. L4 protocol service will be created per router-id & rule-id and the service id will reflect both, as will as the L4 protocol. This will allow the cleanup of the service by tags when the router is detached. """ ip_version = rule.get('ip_version', 4) if rule.get('protocol'): tags = self.nsxpolicy.build_v3_tags_payload( rule, resource_type='os-neutron-fwrule-id', project_name=project_id) tags = nsxlib_utils.add_v3_tag(tags, ROUTER_FW_TAG, router_id) l4_protocol = v3_utils.translate_fw_rule_protocol( rule.get('protocol')) srv_name = 'FW_rule_%s_%s_service' % (rule['id'], rule['protocol']) description = '%s service for FW rule %s of Tier1 %s' % ( rule['protocol'], rule['id'], router_id) if l4_protocol in [nsx_constants.TCP, nsx_constants.UDP]: if rule.get('destination_port') is None: destination_ports = [] else: destination_ports = v3_utils.translate_fw_rule_ports( rule['destination_port']) if rule.get('source_port') is None: source_ports = [] else: source_ports = v3_utils.translate_fw_rule_ports( rule['source_port']) srv_id = self.nsxpolicy.service.create_or_overwrite( srv_name, description=description, protocol=l4_protocol, dest_ports=destination_ports, source_ports=source_ports, tags=tags) elif l4_protocol == nsx_constants.ICMPV4: #TODO(asarfaty): Can use predefined service for ICMP srv_id = self.nsxpolicy.icmp_service.create_or_overwrite( srv_name, version=ip_version, tags=tags) return srv_id def _get_random_rule_id(self, rule_id): """Return a rule ID with random suffix to be used on the NSX Random sequence needs to be added to rule IDs, so that PUT command will replace all existing rules. Keeping the same rule id will require updating the rule revision as well. """ #TODO(asarfaty): add support for self created id in build_entry and # remove this method return '%s-%s' % (rule_id, str(random.randint(1, 10000000))) def _get_rule_ips_group_id(self, rule_id, direction): return '%s-%s' % (direction, rule_id) def _is_empty_cidr(self, cidr, fwaas_rule_id): net = netaddr.IPNetwork(cidr) if ((net.version == 4 and cidr.startswith('0.0.0.0')) or (net.version == 6 and str(net.ip) == "::")): LOG.warning("Unsupported FWaaS cidr %(cidr)s for rule %(id)s", {'cidr': cidr, 'id': fwaas_rule_id}) return True def _validate_cidr(self, cidr, fwaas_rule_id): error_msg = (_("Illegal FWaaS cidr %(cidr)s for rule %(id)s") % {'cidr': cidr, 'id': fwaas_rule_id}) # Validate that this is a legal & supported ipv4 / ipv6 cidr net = netaddr.IPNetwork(cidr) if net.version == 4: if net.prefixlen == 0: LOG.error(error_msg) raise self.driver_exception(driver=self.driver_name) elif net.version == 6: if net.prefixlen == 0: LOG.error(error_msg) raise self.driver_exception(driver=self.driver_name) else: LOG.error(error_msg) raise self.driver_exception(driver=self.driver_name) def _get_rule_cidr_group(self, project_id, router_id, rule, is_source, is_ingress): field = 'source_ip_address' if is_source else 'destination_ip_address' direction_text = 'source' if is_source else 'destination' if (rule.get(field) and not self._is_empty_cidr(rule[field], rule['id'])): # Create a group for ips group_ips = rule[field] group_id = self._get_rule_ips_group_id(rule['id'], direction_text) self._validate_cidr(group_ips, rule['id']) expr = self.nsxpolicy.group.build_ip_address_expression( [group_ips]) tags = self.nsxpolicy.build_v3_tags_payload( rule, resource_type='os-neutron-fwrule-id', project_name=project_id) tags = nsxlib_utils.add_v3_tag(tags, ROUTER_FW_TAG, router_id) self.nsxpolicy.group.create_or_overwrite_with_conditions( "FW_rule_%s_%s" % (rule['id'], direction_text), policy_constants.DEFAULT_DOMAIN, group_id=group_id, description='%s: %s' % (direction_text, group_ips), conditions=[expr], tags=tags) return group_id def update_segment_group(self, context, router_id, neutron_net_id): """Update the segment group for fwaas rules in case fip changed""" try: group_id = '%s-%s' % (router_id, neutron_net_id) self.nsxpolicy.group.get(policy_constants.DEFAULT_DOMAIN, group_id, silent=True) except Exception: # no relevant group needs to be updated return self._create_network_group(context, router_id, neutron_net_id) def _create_network_group(self, context, router_id, neutron_net_id): scope_and_tag = "%s|%s" % ('os-neutron-net-id', neutron_net_id) tags = [] tags = nsxlib_utils.add_v3_tag(tags, ROUTER_FW_TAG, router_id) if cfg.CONF.nsx_p.firewall_match_internal_addr: expr = self.nsxpolicy.group.build_condition( cond_val=scope_and_tag, cond_key=policy_constants.CONDITION_KEY_TAG, cond_member_type=nsx_constants.TARGET_TYPE_LOGICAL_SWITCH) else: # Need to add fips to the network cidr group_ips = [] subnets = self.core_plugin.get_subnets_by_network( context.elevated(), neutron_net_id) for subnet in subnets: group_ips.append(subnet['cidr']) filters = { 'network_id': [neutron_net_id], 'device_owner': ['compute:nova'] } vm_ports = self.core_plugin.get_ports(context.elevated(), filters) for vm_port in vm_ports: fip_filter = {'port_id': [vm_port['id']]} fips = self.core_plugin.get_floatingips( context.elevated(), fip_filter) for fip in fips: group_ips.append(fip['floating_ip_address']) expr = self.nsxpolicy.group.build_ip_address_expression( group_ips) group_id = '%s-%s' % (router_id, neutron_net_id) self.nsxpolicy.group.create_or_overwrite_with_conditions( "Segment_%s" % neutron_net_id, policy_constants.DEFAULT_DOMAIN, group_id=group_id, description='Group for segment %s' % neutron_net_id, conditions=[expr], tags=tags) return group_id def _translate_rules(self, project_id, router_id, segment_group, fwaas_rules, is_ingress, logged=False): """Translate a list of FWaaS rules to NSX rule structure""" translated_rules = [] for rule in fwaas_rules: if not rule['enabled']: # skip disabled rules continue # Make sure the rule has a name, and it starts with the prefix # (backend max name length is 255) if rule.get('name'): rule_name = RULE_NAME_PREFIX + rule['name'] else: rule_name = RULE_NAME_PREFIX + rule['id'] rule_name = rule_name[:255] # Set rule ID with a random suffix rule_id = self._get_random_rule_id(rule['id']) action = v3_utils.translate_fw_rule_action( rule['action'], rule['id']) if not action: raise exceptions.FirewallInternalDriverError( driver=self.internal_driver.driver_name) src_group = self._get_rule_cidr_group( project_id, router_id, rule, is_source=True, is_ingress=is_ingress) if not is_ingress and not src_group: src_group = segment_group dest_group = self._get_rule_cidr_group( project_id, router_id, rule, is_source=False, is_ingress=is_ingress) if is_ingress and not dest_group: dest_group = segment_group srv_id = self._translate_service(project_id, router_id, rule) direction = nsx_constants.IN if is_ingress else nsx_constants.OUT ip_protocol = (nsx_constants.IPV4 if rule.get('ip_version', 4) == 4 else nsx_constants.IPV6) rule_entry = self.nsxpolicy.gateway_policy.build_entry( rule_name, policy_constants.DEFAULT_DOMAIN, router_id, rule_id, description=rule.get('description'), action=action, source_groups=[src_group] if src_group else None, dest_groups=[dest_group] if dest_group else None, service_ids=[srv_id] if srv_id else None, ip_protocol=ip_protocol, logged=logged, scope=[self.nsxpolicy.tier1.get_path(router_id)], direction=direction) translated_rules.append(rule_entry) return translated_rules def _get_port_translated_rules(self, context, project_id, router_id, neutron_net_id, firewall_group, plugin_rules): """Return the list of translated FWaaS rules per port Add the egress/ingress rules of this port + default drop rules in each direction for this port. """ net_group_id = self._create_network_group( context, router_id, neutron_net_id) port_rules = [] # Add the firewall group ingress/egress rules only if the fw is up if firewall_group['admin_state_up']: port_rules.extend(self._translate_rules( project_id, router_id, net_group_id, firewall_group['ingress_rule_list'], is_ingress=True)) port_rules.extend(self._translate_rules( project_id, router_id, net_group_id, firewall_group['egress_rule_list'], is_ingress=False)) # Add the per-port plugin rules if plugin_rules and isinstance(plugin_rules, list): port_rules.extend(plugin_rules) # Add ingress/egress block rules for this port port_rules.extend([ self.nsxpolicy.gateway_policy.build_entry( "Block port ingress", policy_constants.DEFAULT_DOMAIN, router_id, self._get_random_rule_id( DEFAULT_RULE_ID + neutron_net_id + 'ingress'), action=nsx_constants.FW_ACTION_DROP, dest_groups=[net_group_id], scope=[self.nsxpolicy.tier1.get_path(router_id)], direction=nsx_constants.IN), self.nsxpolicy.gateway_policy.build_entry( "Block port egress", policy_constants.DEFAULT_DOMAIN, router_id, self._get_random_rule_id( DEFAULT_RULE_ID + neutron_net_id + 'egress'), action=nsx_constants.FW_ACTION_DROP, scope=[self.nsxpolicy.tier1.get_path(router_id)], source_groups=[net_group_id], direction=nsx_constants.OUT)]) return port_rules def _set_rules_order(self, fw_rules): # TODO(asarfaty): Consider adding vmware-nsxlib api for this # add sequence numbers to keep rules in order seq_num = 0 for rule in fw_rules: rule.attrs['sequence_number'] = seq_num seq_num += 1 def update_router_firewall(self, context, router_id, router, router_interfaces, called_from_fw=False): """Rewrite all the FWaaS v2 rules in the router edge firewall This method should be called on FWaaS updates, and on router interfaces changes. The purpose of called_from_fw is to differ between fw calls and other router calls, and if it is True - add the service router accordingly. """ plugin = self.core_plugin project_id = router['project_id'] fw_rules = [] router_with_fw = False # Add firewall rules per port attached to a firewall group for port in router_interfaces: # Check if this port has a firewall fwg = self.get_port_fwg(context, port['id']) if fwg: router_with_fw = True # Add plugin additional allow rules plugin_rules = self.core_plugin.get_extra_fw_rules( context, router_id, port['id']) # Add the FWaaS rules for this port:ingress/egress firewall # rules + default ingress/egress drop rule for this port fw_rules.extend(self._get_port_translated_rules( context, project_id, router_id, port['network_id'], fwg, plugin_rules)) # Add a default allow-all rule to all other traffic & ports fw_rules.append(self._get_default_backend_rule(router_id)) self._set_rules_order(fw_rules) # Update the backend router firewall sr_exists_on_backend = plugin.verify_sr_at_backend(router_id) if called_from_fw: # FW action required if router_with_fw: # Firewall needed and no NSX service router: create it. if not sr_exists_on_backend: plugin.create_service_router( context, router_id, update_firewall=False) sr_exists_on_backend = True else: # First, check if other services exist and use the sr router_with_services = plugin.service_router_has_services( context, router_id, router=router) if not router_with_services and sr_exists_on_backend: # No other services that require service router: delete it # This also deleted the gateway policy. self.core_plugin.delete_service_router(router_id) sr_exists_on_backend = False if sr_exists_on_backend: if router_with_fw: self.create_or_update_router_gateway_policy(context, router_id, router, fw_rules) else: # Do all the cleanup once the router has no more FW rules # create or update the edge firewall # TODO(asarfaty): Consider keeping the FW with default allow # rule instead of deletion as it may be created again soon self.delete_router_gateway_policy(router_id) def create_or_update_router_gateway_policy(self, context, router_id, router, fw_rules): """Create/Overwrite gateway policy for a router with firewall rules""" # Check if the gateway policy already exists try: self.nsxpolicy.gateway_policy.get(policy_constants.DEFAULT_DOMAIN, map_id=router_id, silent=True) except nsx_lib_exc.ResourceNotFound: LOG.info("Going to create gateway policy for router %s", router_id) else: # only update the rules of this policy self.nsxpolicy.gateway_policy.update_entries( policy_constants.DEFAULT_DOMAIN, router_id, fw_rules, category=policy_constants.CATEGORY_LOCAL_GW) return tags = self.nsxpolicy.build_v3_tags_payload( router, resource_type='os-neutron-router-id', project_name=context.tenant_name) policy_name = GATEWAY_POLICY_NAME % router_id self.nsxpolicy.gateway_policy.create_with_entries( policy_name, policy_constants.DEFAULT_DOMAIN, map_id=router_id, description=policy_name, tags=tags, entries=fw_rules, category=policy_constants.CATEGORY_LOCAL_GW) def delete_router_gateway_policy(self, router_id): """Delete the gateway policy associated with a router, it it exists. Should be called when the router is deleted / FW removed from it """ try: self.nsxpolicy.gateway_policy.get(policy_constants.DEFAULT_DOMAIN, map_id=router_id, silent=True) except nsx_lib_exc.ResourceNotFound: return self.nsxpolicy.gateway_policy.delete(policy_constants.DEFAULT_DOMAIN, map_id=router_id) # Also delete all groups & services self.cleanup_router_fw_resources(router_id) def cleanup_router_fw_resources(self, router_id): tags_to_search = [{'scope': ROUTER_FW_TAG, 'tag': router_id}] # Delete per rule & per network groups groups = self.nsxpolicy.search_by_tags( tags_to_search, self.nsxpolicy.group.entry_def.resource_type())['results'] for group in groups: self.nsxpolicy.group.delete(policy_constants.DEFAULT_DOMAIN, group['id']) services = self.nsxpolicy.search_by_tags( tags_to_search, self.nsxpolicy.service.parent_entry_def.resource_type())['results'] for srv in services: self.nsxpolicy.service.delete(srv['id'])