vmware-nsx/vmware_nsx/services/fwaas/nsx_p/fwaas_callbacks_v2.py

469 lines
20 KiB
Python

# 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'])