Support l3 stateless firewall based on OVN

This patch implements a driver based on OVN, it creates port_group
for every l3 firewall_group and adds relating ports to port_group,
it also transforms firewall_rules to stateless acls.

Tests will been put in next patch.

NOTE: it depends on ML2/OVN.

Partially-Implements: blueprint support-l3-firewall-for-ovn-driver
Related-Bug: #1971958
Change-Id: If153645b3da198ef1746e98af80ac6f0a0b41bf9
This commit is contained in:
liushy 2022-06-14 14:32:06 +08:00
parent e7b472e6bf
commit 8de0c36cb9
10 changed files with 1150 additions and 1 deletions

View File

@ -38,12 +38,21 @@ function install_fwaas() {
fi
}
function is_ovn_enabled {
[[ $Q_AGENT == "ovn" ]] && return 0
return 1
}
function configure_fwaas_v2() {
# Add conf file
cp $NEUTRON_FWAAS_DIR/etc/neutron_fwaas.conf.sample $NEUTRON_FWAAS_CONF
neutron_server_config_add $NEUTRON_FWAAS_CONF
inicomment $NEUTRON_FWAAS_CONF service_providers service_provider
iniadd $NEUTRON_FWAAS_CONF service_providers service_provider $NEUTRON_FWAAS_SERVICE_PROVIDERV2
if is_ovn_enabled; then
iniadd $NEUTRON_FWAAS_CONF service_providers service_provider $NEUTRON_FWAAS_SERVICE_PROVIDERV2_OVN
else
iniadd $NEUTRON_FWAAS_CONF service_providers service_provider $NEUTRON_FWAAS_SERVICE_PROVIDERV2
fi
neutron_fwaas_configure_driver fwaas_v2
if is_service_enabled q-l3; then

View File

@ -8,5 +8,6 @@ NEUTRON_FWAAS_CONF_FILE=neutron_fwaas.conf
NEUTRON_FWAAS_CONF=$NEUTRON_CONF_DIR/$NEUTRON_FWAAS_CONF_FILE
NEUTRON_FWAAS_SERVICE_PROVIDERV2=${NEUTRON_FWAAS_SERVICE_PROVIDERV2:-FIREWALL_V2:fwaas_db:neutron_fwaas.services.firewall.service_drivers.agents.agents.FirewallAgentDriver:default}
NEUTRON_FWAAS_SERVICE_PROVIDERV2_OVN=${NEUTRON_FWAAS_SERVICE_PROVIDERV2:-FIREWALL_V2:fwaas_db:neutron_fwaas.services.firewall.service_drivers.ovn.firewall_l3_driver.OVNFwaasDriver:default}
enable_service q-fwaas-v2

View File

@ -0,0 +1,237 @@
# Copyright 2022 EasyStack, 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.
from neutron.common.ovn import utils as ovn_utils
from neutron_lib import constants as const
from neutron_fwaas.services.firewall.service_drivers.ovn import \
constants as ovn_const
from neutron_fwaas.services.firewall.service_drivers.ovn import \
exceptions as ovn_fw_exc
def acl_direction(direction, port_group=None):
if direction == const.INGRESS_DIRECTION:
portdir = 'inport'
else:
portdir = 'outport'
return '%s == @%s' % (portdir, port_group)
def acl_ethertype(rule):
match = ''
ip_version = None
icmp = None
if rule['ip_version'] == const.IP_VERSION_4:
match = ' && ip4'
ip_version = 'ip4'
icmp = 'icmp4'
elif rule['ip_version'] == const.IP_VERSION_6:
match = ' && ip6'
ip_version = 'ip6'
icmp = 'icmp6'
return match, ip_version, icmp
def acl_ip(rule, ip_version):
src_ip = rule.get('source_ip_address')
dst_ip = rule.get('destination_ip_address')
src = ' && %s.src == %s' % (ip_version, src_ip) if src_ip else ''
dst = ' && %s.dst == %s' % (ip_version, dst_ip) if dst_ip else ''
return src + dst
def get_min_max_ports_from_range(port_range):
if not port_range:
return [None, None]
min_port, sep, max_port = port_range.partition(":")
if not max_port:
max_port = min_port
return [int(min_port), int(max_port)]
def acl_protocol_ports(protocol, port_range, is_dst=True):
match = ''
min_port, max_port = get_min_max_ports_from_range(port_range)
dir = 'dst' if is_dst else 'src'
if protocol in ovn_const.TRANSPORT_PROTOCOLS:
if min_port is not None and min_port == max_port:
match += ' && %s.%s == %d' % (protocol, dir, min_port)
else:
if min_port is not None:
match += ' && %s.%s >= %d' % (protocol, dir, min_port)
if max_port is not None:
match += ' && %s.%s <= %d' % (protocol, dir, max_port)
return match
def acl_protocol_and_ports(rule, icmp):
match = ''
protocol = rule.get('protocol')
if protocol is None:
return match
src_port = rule.get('source_port')
dst_port = rule.get('destination_port')
if protocol in ovn_const.TRANSPORT_PROTOCOLS:
match += ' && %s' % protocol
match += acl_protocol_ports(protocol, src_port, is_dst=False)
match += acl_protocol_ports(protocol, dst_port)
elif protocol in ovn_const.ICMP_PROTOCOLS:
protocol = icmp
match += ' && %s' % protocol
return match
def acl_action_and_priority(rule, direction):
action = rule['action']
pos = rule.get('position', 0)
if action == 'deny' and rule.get(ovn_const.DEFAULT_RULE, False):
return (ovn_const.ACL_ACTION_DROP,
ovn_const.ACL_PRIORITY_DEFAULT)
if direction == const.INGRESS_DIRECTION:
priority = ovn_const.ACL_PRIORITY_INGRESS
else:
priority = ovn_const.ACL_PRIORITY_EGRESS
if action == 'allow':
return (ovn_const.ACL_ACTION_ALLOW_STATELESS,
priority - pos)
elif action == 'deny':
return (ovn_const.ACL_ACTION_DROP,
priority - pos)
elif action == 'reject':
return (ovn_const.ACL_ACTION_REJECT,
priority - pos)
def acl_entry_for_port_group(port_group, rule, direction, match):
dir_map = {const.INGRESS_DIRECTION: 'from-lport',
const.EGRESS_DIRECTION: 'to-lport'}
action, priority = acl_action_and_priority(rule, direction)
acl = {"port_group": port_group,
"priority": priority,
"action": action,
"log": False,
"name": [],
"severity": [],
"direction": dir_map[direction],
"match": match,
ovn_const.OVN_FWR_EXT_ID_KEY: rule['id']}
return acl
def get_rule_acl_for_port_group(port_group, rule, direction):
match = acl_direction(direction, port_group=port_group)
ip_match, ip_version, icmp = acl_ethertype(rule)
match += ip_match
match += acl_ip(rule, ip_version)
match += acl_protocol_and_ports(rule, icmp)
return acl_entry_for_port_group(port_group, rule, direction, match)
def update_ports_for_pg(nb_idl, txn, pg_name, ports_add=None,
ports_delete=None):
if ports_add is None:
ports_add = []
if ports_delete is None:
ports_delete = []
# Add ports to port_group
for port_id in ports_add:
txn.add(nb_idl.pg_add_ports(
pg_name, port_id))
for port_id in ports_delete:
txn.add(nb_idl.pg_del_ports(
pg_name, port_id, if_exists=True))
def get_default_acls_for_pg(nb_idl, pg_name):
nb_acls = nb_idl.pg_acl_list(pg_name).execute(check_error=True)
default_acl_list = []
for nb_acl in nb_acls:
# Get acl whose external_ids has firewall_rule_id, then
# append it to list if its value equal to default_rule_id
ext_ids = getattr(nb_acl, 'external_ids', {})
if (ext_ids.get(ovn_const.OVN_FWR_EXT_ID_KEY) ==
ovn_const.DEFAULT_RULE_ID):
default_acl_list.append(nb_acl.uuid)
return default_acl_list
def process_rule_for_pg(nb_idl, txn, pg_name, rule, direction,
op=ovn_const.OP_ADD):
dir_map = {const.INGRESS_DIRECTION: 'from-lport',
const.EGRESS_DIRECTION: 'to-lport'}
supported_ops = [ovn_const.OP_ADD, ovn_const.OP_DEL,
ovn_const.OP_MOD]
if op not in supported_ops:
raise ovn_fw_exc.OperatorNotSupported(
operator=op, valid_operators=supported_ops)
acl = get_rule_acl_for_port_group(
pg_name, rule, direction)
# Add acl
if op == ovn_const.OP_ADD:
txn.add(nb_idl.pg_acl_add(**acl, may_exist=True))
# Modify/Delete acl
else:
nb_acls = nb_idl.pg_acl_list(pg_name).execute(check_error=True)
for nb_acl in nb_acls:
# Get acl whose external_ids has firewall_rule_id,
# then change it if its value equal to rule's
ext_ids = getattr(nb_acl, 'external_ids', {})
if (ext_ids.get(ovn_const.OVN_FWR_EXT_ID_KEY) ==
rule['id'] and dir_map[direction] == nb_acl.direction):
if op == ovn_const.OP_MOD:
txn.add(nb_idl.db_set(
'ACL', nb_acl.uuid,
('match', acl['match']),
('action', acl['action'])))
elif op == ovn_const.OP_DEL:
txn.add(nb_idl.pg_acl_del(
acl['port_group'],
acl['direction'],
nb_acl.priority,
acl['match']))
break
def create_pg_for_fwg(nb_idl, fwg_id):
pg_name = ovn_utils.ovn_port_group_name(fwg_id)
# Add port_group
with nb_idl.transaction(check_error=True) as txn:
ext_ids = {ovn_const.OVN_FWG_EXT_ID_KEY: fwg_id}
txn.add(nb_idl.pg_add(name=pg_name, acls=[],
external_ids=ext_ids))
def add_default_acls_for_pg(nb_idl, txn, pg_name):
# Traffic is default denied, ipv4 or ipv6 with two directions,
# so number of default acls is 4
default_rule_v4 = {'action': 'deny', 'ip_version': 4,
'id': ovn_const.DEFAULT_RULE_ID,
ovn_const.DEFAULT_RULE: True}
default_rule_v6 = {'action': 'deny', 'ip_version': 6,
'id': ovn_const.DEFAULT_RULE_ID,
ovn_const.DEFAULT_RULE: True}
for dir in [const.EGRESS_DIRECTION, const.INGRESS_DIRECTION]:
process_rule_for_pg(nb_idl, txn, pg_name,
default_rule_v4,
dir,
op=ovn_const.OP_ADD)
process_rule_for_pg(nb_idl, txn, pg_name,
default_rule_v6,
dir,
op=ovn_const.OP_ADD)

View File

@ -0,0 +1,44 @@
# Copyright 2022 EasyStack, 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.
from neutron_lib import constants as const
OVN_FWG_EXT_ID_KEY = 'neutron:firewall_group_id'
OVN_FWR_EXT_ID_KEY = 'neutron:firewall_rule_id'
ACL_ACTION_DROP = 'drop'
ACL_ACTION_REJECT = 'reject'
ACL_ACTION_ALLOW_STATELESS = 'allow-stateless'
ACL_ACTION_ALLOW = 'allow'
ACL_PRIORITY_INGRESS = 2000
ACL_PRIORITY_EGRESS = 2000
ACL_PRIORITY_DEFAULT = 1001
OP_ADD = 'add'
OP_DEL = 'del'
OP_MOD = 'mod'
DEFAULT_RULE = 'is_default'
DEFAULT_RULE_ID = 'default_rule'
# Drop acls of ipv4 or ipv6 with two directions, so number of
# default acls is 4
DEFAULT_ACL_NUM = 4
# Group of transport protocols supported
TRANSPORT_PROTOCOLS = (const.PROTO_NAME_TCP,
const.PROTO_NAME_UDP,
const.PROTO_NAME_SCTP)
# Group of versions of the ICMP protocol supported
ICMP_PROTOCOLS = (const.PROTO_NAME_ICMP,
const.PROTO_NAME_IPV6_ICMP)

View File

@ -0,0 +1,33 @@
# Copyright 2022 EasyStack, 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.
from neutron._i18n import _
from neutron_lib import exceptions as n_exc
class MechanismDriverNotFound(n_exc.NotFound):
message = _("None of the supported mechanism drivers found: "
"%(mechanism_drivers)s. Check your configuration.")
class ProtocolNotSupported(n_exc.NeutronException):
message = _('The protocol "%(protocol)s" is not supported. Valid '
'protocols are: %(valid_protocols)s; or protocol '
'numbers ranging from 0 to 255.')
class OperatorNotSupported(n_exc.NeutronException):
message = _('The operator "%(operator)s" is not supported. Valid '
'operators are: %(valid_operators)s.')

View File

@ -0,0 +1,358 @@
# Copyright 2022 EasyStack, 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.
from neutron.common.ovn import utils as ovn_utils
from neutron_lib import constants as const
from oslo_log import log as logging
from neutron_fwaas.services.firewall.service_drivers import driver_api
from neutron_fwaas.services.firewall.service_drivers.ovn import \
acl as ovn_acl
from neutron_fwaas.services.firewall.service_drivers.ovn import \
constants as ovn_const
from neutron_fwaas.services.firewall.service_drivers.ovn import \
exceptions as ovn_fw_exc
LOG = logging.getLogger(__name__)
class OVNFwaasDriver(driver_api.FirewallDriverDB):
"""OVN l3 acl driver to implement
Depends on ml2/ovn, use ovn_client to put acl rules to the lsp which
is a peer of the lrp.
"""
def __init__(self, service_plugin):
super(OVNFwaasDriver, self).__init__(service_plugin)
self._mech = None
def is_supported_l2_port(self, port):
return False
def is_supported_l3_port(self, port):
return True
def start_rpc_listener(self):
return []
@property
def _nb_ovn(self):
return self._mech_driver.nb_ovn
@property
def _mech_driver(self):
if self._mech is None:
drivers = ('ovn', 'ovn-sync')
for driver in drivers:
try:
self._mech = \
self._core_plugin.mechanism_manager.mech_drivers[
driver].obj
break
except KeyError:
pass
else:
raise ovn_fw_exc.MechanismDriverNotFound(
mechanism_drivers=drivers)
return self._mech
def _init_firewall_group(self, txn, fwg_id):
"""Add port_group for firewall_group
After create port_group for fwg, add default drop acls to it
"""
pg_name = ovn_utils.ovn_port_group_name(fwg_id)
ovn_acl.create_pg_for_fwg(self._nb_ovn, fwg_id)
ovn_acl.add_default_acls_for_pg(self._nb_ovn, txn, pg_name)
LOG.info("Successfully created port_group for firewall_group: %s",
fwg_id)
def _add_rules_for_firewall_group(self, context, txn, fwg_id,
rule_id=None):
"""Add all rules belong to firewall_group
"""
fwg_with_rules = \
self.firewall_db.make_firewall_group_dict_with_rules(
context, fwg_id)
egress_rule_list = fwg_with_rules['egress_rule_list']
ingress_rule_list = fwg_with_rules['ingress_rule_list']
pg_name = ovn_utils.ovn_port_group_name(fwg_id)
rule_map = {const.INGRESS_DIRECTION: ingress_rule_list,
const.EGRESS_DIRECTION: egress_rule_list}
for dir, rule_list in rule_map.items():
position = 0
for rule in rule_list:
rule['position'] = position
position += 1
if not rule['enabled']:
continue
if rule_id:
# For specify rule id
if rule_id == rule['id']:
ovn_acl.process_rule_for_pg(self._nb_ovn, txn,
pg_name, rule, dir,
op=ovn_const.OP_ADD)
LOG.info("Successfully enable rule %(rule)s to "
"firewall_group %(fwg)s",
{"rule": rule_id,
"fwg": fwg_id})
break
else:
ovn_acl.process_rule_for_pg(self._nb_ovn, txn, pg_name,
rule, dir,
op=ovn_const.OP_ADD)
LOG.info("Successfully added rules for firewall_group %s",
fwg_id)
def _clear_rules_for_firewall_group(self, context, txn, fwg_id):
"""Clear acls belong to firewall_group
Delete all rule acls but remain the default acls
"""
pg_name = ovn_utils.ovn_port_group_name(fwg_id)
default_acls = ovn_acl.get_default_acls_for_pg(self._nb_ovn, pg_name)
if len(default_acls) == ovn_const.DEFAULT_ACL_NUM:
txn.add(self._nb_ovn.db_set(
'Port_Group', pg_name,
('acls', default_acls)))
else:
ovn_acl.add_default_acls_for_pg(self._nb_ovn, txn, pg_name)
LOG.info("Successfully clear rules for firewall_group %s",
fwg_id)
def _process_acls_by_policies_or_rule(self, context, policy_ids,
rule_info=None,
op=ovn_const.OP_ADD):
"""Delete/Update/Add the acls by rule or policies
"""
ing_fwg_list = []
eg_fwg_list = []
if not policy_ids:
return
for policy_id in policy_ids:
ing_fwg_ids, eg_fwg_ids = self.firewall_db.get_fwgs_with_policy(
context, policy_id)
ing_fwg_list += ing_fwg_ids
eg_fwg_list += eg_fwg_ids
if not rule_info and op == ovn_const.OP_ADD:
# Add acls
rule_info = {}
for fwg_id in list(set(ing_fwg_list + eg_fwg_list)):
pg_name = ovn_utils.ovn_port_group_name(fwg_id)
with self._nb_ovn.transaction(check_error=True) as txn:
if self._nb_ovn.get_port_group(pg_name):
self._clear_rules_for_firewall_group(context, txn,
fwg_id)
else:
self._init_firewall_group(txn, fwg_id)
self._add_rules_for_firewall_group(context, txn, fwg_id)
elif rule_info and op == ovn_const.OP_ADD:
# Process the rule when enabled
for fwg_id in list(set(ing_fwg_list + eg_fwg_list)):
pg_name = ovn_utils.ovn_port_group_name(fwg_id)
with self._nb_ovn.transaction(check_error=True) as txn:
if self._nb_ovn.get_port_group(pg_name):
self._add_rules_for_firewall_group(context, txn,
fwg_id,
rule_info['id'])
elif rule_info:
# Delete/Update acls
fwg_map = {const.INGRESS_DIRECTION: list(set(ing_fwg_list)),
const.EGRESS_DIRECTION: list(set(eg_fwg_list))}
for dir, fwg_list in fwg_map.items():
for fwg_id in fwg_list:
pg_name = ovn_utils.ovn_port_group_name(fwg_id)
with self._nb_ovn.transaction(check_error=True) as txn:
if not self._nb_ovn.get_port_group(pg_name):
LOG.warning("Cannot find Port_Group with name: %s",
pg_name)
continue
ovn_acl.process_rule_for_pg(self._nb_ovn, txn,
pg_name, rule_info,
dir, op=op)
LOG.info("Successfully %(op)s acls by rule %(rule)s "
"and policies %(p_ids)s",
{"op": op,
"rule": rule_info.get('id'),
"p_ids": policy_ids})
def create_firewall_group_precommit(self, context, firewall_group):
if not firewall_group['ports']:
LOG.info("No ports bound to firewall_group: %s, "
"set it to inactive", firewall_group['id'])
status = const.INACTIVE
else:
status = const.PENDING_CREATE
with self._nb_ovn.transaction(check_error=True) as txn:
self._init_firewall_group(txn, firewall_group['id'])
firewall_group['status'] = status
def create_firewall_group_postcommit(self, context, firewall_group):
pg_name = ovn_utils.ovn_port_group_name(firewall_group['id'])
try:
with self._nb_ovn.transaction(check_error=True) as txn:
if (firewall_group['ingress_firewall_policy_id'] or
firewall_group['egress_firewall_policy_id']):
# Add rule acls to port_group
self._add_rules_for_firewall_group(context, txn,
firewall_group['id'])
if firewall_group['ports']:
# Add ports to port_group
ovn_acl.update_ports_for_pg(self._nb_ovn,
txn, pg_name,
firewall_group['ports'])
firewall_group['status'] = const.ACTIVE
LOG.info("Successfully added ports for firewall_group %s",
firewall_group['id'])
except Exception:
with self._nb_ovn.transaction(check_error=True) as txn:
if self._nb_ovn.get_port_group(pg_name):
txn.add(self._nb_ovn.pg_del(name=pg_name, if_exists=True))
LOG.error("Failed to create_firewall_group_postcommit.")
raise
else:
self.firewall_db.update_firewall_group_status(
context, firewall_group['id'], firewall_group['status'])
def update_firewall_group_precommit(self, context, old_firewall_group,
new_firewall_group):
port_updated = (set(new_firewall_group['ports']) !=
set(old_firewall_group['ports']))
policies_updated = (
new_firewall_group['ingress_firewall_policy_id'] !=
old_firewall_group['ingress_firewall_policy_id'] or
new_firewall_group['egress_firewall_policy_id'] !=
old_firewall_group['egress_firewall_policy_id']
)
if port_updated or policies_updated:
new_firewall_group['status'] = const.PENDING_UPDATE
def update_firewall_group_postcommit(self, context, old_firewall_group,
new_firewall_group):
if new_firewall_group['status'] != const.PENDING_UPDATE:
return
old_ports = set(old_firewall_group['ports'])
new_ports = set(new_firewall_group['ports'])
old_ing_policy = old_firewall_group['ingress_firewall_policy_id']
new_ing_policy = new_firewall_group['ingress_firewall_policy_id']
old_eg_policy = old_firewall_group['egress_firewall_policy_id']
new_eg_policy = new_firewall_group['egress_firewall_policy_id']
pg_name = ovn_utils.ovn_port_group_name(new_firewall_group['id'])
# We except it would be active
# If no ports, set it to inactive
new_firewall_group['status'] = const.ACTIVE
if not new_ports:
LOG.info("No ports bound to firewall_group: %s, "
"set it to inactive", new_firewall_group['id'])
new_firewall_group['status'] = const.INACTIVE
# If port_group is not exist, recreate it,
# add acls and ports.
if not self._nb_ovn.get_port_group(pg_name):
with self._nb_ovn.transaction(check_error=True) as txn:
self._init_firewall_group(txn, new_firewall_group['id'])
if new_ports:
ovn_acl.update_ports_for_pg(self._nb_ovn, txn,
pg_name, new_ports)
if new_ing_policy or new_eg_policy:
self._add_rules_for_firewall_group(
context, txn, new_firewall_group['id'])
else:
with self._nb_ovn.transaction(check_error=True) as txn:
# Process changes of ports
if old_ports != new_ports:
ports_add = list(new_ports - old_ports)
ports_delete = list(old_ports - new_ports)
ovn_acl.update_ports_for_pg(self._nb_ovn, txn, pg_name,
ports_add, ports_delete)
# Process changes of policies
if (old_ing_policy != new_ing_policy or
old_eg_policy != new_eg_policy):
# Clear rules first
self._clear_rules_for_firewall_group(
context, txn, new_firewall_group['id'])
# Add rules if it has
if new_ing_policy or new_eg_policy:
self._add_rules_for_firewall_group(
context, txn, new_firewall_group['id'])
self.firewall_db.update_firewall_group_status(
context, new_firewall_group['id'],
new_firewall_group['status'])
def delete_firewall_group_precommit(self, context, firewall_group):
pg_name = ovn_utils.ovn_port_group_name(firewall_group['id'])
with self._nb_ovn.transaction(check_error=True) as txn:
if self._nb_ovn.get_port_group(pg_name):
txn.add(self._nb_ovn.pg_del(name=pg_name, if_exists=True))
def update_firewall_policy_postcommit(self, context, old_firewall_policy,
new_firewall_policy):
old_rules = old_firewall_policy['firewall_rules']
new_rules = new_firewall_policy['firewall_rules']
if old_rules == new_rules:
return
self._process_acls_by_policies_or_rule(context,
[new_firewall_policy['id']])
def update_firewall_rule_postcommit(self, context, old_firewall_rule,
new_firewall_rule):
NEED_UPDATE_FIELDS = ['enabled', 'protocol', 'ip_version',
'source_ip_address', 'destination_ip_address',
'source_port', 'destination_port', 'action']
need_update = False
for field in NEED_UPDATE_FIELDS:
if old_firewall_rule[field] != new_firewall_rule[field]:
need_update = True
if not need_update:
return
firewall_policy_ids = old_firewall_rule.get('firewall_policy_id')
# If rule is enabled, its acls should be inserted
# If rule is disabled, its acls should be removed
# If rule is always disabled, nothing to do
if not old_firewall_rule['enabled'] and new_firewall_rule['enabled']:
self._process_acls_by_policies_or_rule(context,
firewall_policy_ids,
new_firewall_rule)
return
elif old_firewall_rule['enabled'] and not new_firewall_rule['enabled']:
self._process_acls_by_policies_or_rule(context,
firewall_policy_ids,
old_firewall_rule,
ovn_const.OP_DEL)
return
elif not new_firewall_rule['enabled']:
return
# Process changes of rule
self._process_acls_by_policies_or_rule(
context, firewall_policy_ids, new_firewall_rule, ovn_const.OP_MOD)
LOG.info("Successfully updated acls for rule: %s",
new_firewall_rule['id'])
def insert_rule_postcommit(self, context, policy_id, rule_info):
# Add acls by policy_id
self._process_acls_by_policies_or_rule(context, [policy_id])
def remove_rule_postcommit(self, context, policy_id, rule_info):
rule_detail = self.firewall_db.get_firewall_rule(
context, rule_info['firewall_rule_id'])
self._process_acls_by_policies_or_rule(
context, [policy_id], rule_detail, ovn_const.OP_DEL)

View File

@ -0,0 +1,452 @@
# Copyright 2022 EasyStack, 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.
from unittest import mock
from neutron import extensions as neutron_extensions
from neutron.tests.unit.extensions import test_l3
from neutron.tests.unit import fake_resources as fakes
from neutron_lib import constants as nl_constants
from neutron_lib import context
from neutron_fwaas.services.firewall.service_drivers import driver_api
from neutron_fwaas.services.firewall.service_drivers.ovn import \
firewall_l3_driver as ovn_driver
from neutron_fwaas.tests.unit.services.firewall import test_fwaas_plugin_v2
OVN_FWAAS_DRIVER = ('neutron_fwaas.services.firewall.service_drivers.'
'ovn.firewall_l3_driver.OVNFwaasDriver')
class TestOVNFwaasDriver(test_fwaas_plugin_v2.FirewallPluginV2TestCase,
test_l3.L3NatTestCaseMixin):
def setUp(self):
l3_plugin_str = ('neutron.tests.unit.extensions.test_l3.'
'TestL3NatServicePlugin')
l3_plugin = {'l3_plugin_name': l3_plugin_str}
super(TestOVNFwaasDriver, self).setUp(
service_provider=OVN_FWAAS_DRIVER,
extra_service_plugins=l3_plugin,
extra_extension_paths=neutron_extensions.__path__)
self.db = self.plugin.driver.firewall_db
self.mech_driver = mock.MagicMock()
self.nb_ovn = fakes.FakeOvsdbNbOvnIdl()
self.sb_ovn = fakes.FakeOvsdbSbOvnIdl()
self.mech_driver._nb_ovn = self.nb_ovn
self.mech_driver._sb_ovn = self.sb_ovn
@property
def _self_context(self):
return context.Context('', self._tenant_id)
def test_create_firewall_group_ports_not_specified(self):
with self.firewall_policy(as_admin=True) as fwp, \
mock.patch.object(driver_api.FirewallDriver,
'_core_plugin') as mock_ml2:
fwp_id = fwp['firewall_policy']['id']
mock_ml2.mechanism_manager = mock.MagicMock()
mock_ml2.mechanism_manager.mech_drivers = {
'ovn': self.mech_driver}
with self.firewall_group(
name='test',
ingress_firewall_policy_id=fwp_id,
egress_firewall_policy_id=fwp_id,
admin_state_up=True,
as_admin=True) as fwg1:
self.assertEqual(nl_constants.INACTIVE,
fwg1['firewall_group']['status'])
def test_create_firewall_group_with_ports(self):
with self.router(name='router1', admin_state_up=True,
tenant_id=self._tenant_id, as_admin=True) as r, \
self.subnet(as_admin=True) as s1, \
self.subnet(cidr='20.0.0.0/24', as_admin=True) as s2:
body = self._router_interface_action(
'add',
r['router']['id'],
s1['subnet']['id'],
None,
as_admin=True)
port_id1 = body['port_id']
body = self._router_interface_action(
'add',
r['router']['id'],
s2['subnet']['id'],
None,
as_admin=True)
port_id2 = body['port_id']
fwg_ports = [port_id1, port_id2]
with self.firewall_policy(do_delete=False,
as_admin=True) as fwp, \
mock.patch.object(ovn_driver.OVNFwaasDriver,
'_nb_ovn') as mock_nb_ovn:
fwp_id = fwp['firewall_policy']['id']
mock_nb_ovn.return_value = self.nb_ovn
with self.firewall_group(
name='test',
ingress_firewall_policy_id=fwp_id,
egress_firewall_policy_id=fwp_id,
ports=fwg_ports, admin_state_up=True,
do_delete=False, as_admin=True) as fwg1:
self.assertEqual(nl_constants.ACTIVE,
fwg1['firewall_group']['status'])
def test_update_firewall_group_with_new_ports(self):
with self.router(name='router1', admin_state_up=True,
tenant_id=self._tenant_id, as_admin=True) as r, \
self.subnet(as_admin=True) as s1, \
self.subnet(cidr='20.0.0.0/24', as_admin=True) as s2, \
self.subnet(cidr='30.0.0.0/24', as_admin=True) as s3:
body = self._router_interface_action(
'add',
r['router']['id'],
s1['subnet']['id'],
None,
as_admin=True)
port_id1 = body['port_id']
body = self._router_interface_action(
'add',
r['router']['id'],
s2['subnet']['id'],
None,
as_admin=True)
port_id2 = body['port_id']
body = self._router_interface_action(
'add',
r['router']['id'],
s3['subnet']['id'],
None,
as_admin=True)
port_id3 = body['port_id']
fwg_ports = [port_id1, port_id2]
with self.firewall_policy(do_delete=False,
as_admin=True) as fwp, \
mock.patch.object(ovn_driver.OVNFwaasDriver,
'_nb_ovn') as mock_nb_ovn:
fwp_id = fwp['firewall_policy']['id']
mock_nb_ovn.return_value = self.nb_ovn
with self.firewall_group(
name='test',
ingress_firewall_policy_id=fwp_id,
egress_firewall_policy_id=fwp_id,
ports=fwg_ports, admin_state_up=True,
do_delete=False, as_admin=True) as fwg1:
self.assertEqual(nl_constants.ACTIVE,
fwg1['firewall_group']['status'])
data = {'firewall_group': {'ports': [port_id2, port_id3]}}
req = self.new_update_request('firewall_groups', data,
fwg1['firewall_group']['id'],
context=self._self_context,
as_admin=True)
res = self.deserialize(self.fmt,
req.get_response(self.ext_api))
self.assertEqual(sorted([port_id2, port_id3]),
sorted(res['firewall_group']['ports']))
self.assertEqual(nl_constants.ACTIVE,
res['firewall_group']['status'])
def test_update_firewall_group_with_ports_and_policy(self):
with self.router(name='router1', admin_state_up=True,
tenant_id=self._tenant_id, as_admin=True) as r, \
self.subnet(as_admin=True) as s1, \
self.subnet(cidr='20.0.0.0/24', as_admin=True) as s2:
body = self._router_interface_action(
'add',
r['router']['id'],
s1['subnet']['id'],
None,
as_admin=True)
port_id1 = body['port_id']
body = self._router_interface_action(
'add',
r['router']['id'],
s2['subnet']['id'],
None,
as_admin=True)
port_id2 = body['port_id']
fwg_ports = [port_id1, port_id2]
with self.firewall_rule(do_delete=False, as_admin=True) as fwr, \
mock.patch.object(ovn_driver.OVNFwaasDriver,
'_nb_ovn') as mock_nb_ovn:
mock_nb_ovn.return_value = self.nb_ovn
with self.firewall_policy(
firewall_rules=[fwr['firewall_rule']['id']],
do_delete=False,
as_admin=True) as fwp:
with self.firewall_group(
name='test',
default_policy=False,
ports=fwg_ports,
admin_state_up=True,
do_delete=False,
as_admin=True) as fwg1:
self.assertEqual(nl_constants.ACTIVE,
fwg1['firewall_group']['status'])
fwp_id = fwp["firewall_policy"]["id"]
data = {'firewall_group': {'ports': fwg_ports}}
req = (self.
new_update_request('firewall_groups', data,
fwg1['firewall_group']['id'],
context=self._self_context,
as_admin=True))
res = self.deserialize(self.fmt,
req.get_response(self.ext_api))
self.assertEqual(nl_constants.ACTIVE,
res['firewall_group']['status'])
data = {'firewall_group': {
'ingress_firewall_policy_id': fwp_id}}
req = (self.
new_update_request('firewall_groups', data,
fwg1['firewall_group']['id'],
context=self._self_context,
as_admin=True))
res = self.deserialize(self.fmt,
req.get_response(self.ext_api))
self.assertEqual(nl_constants.ACTIVE,
res['firewall_group']['status'])
def test_update_firewall_policy_with_new_rules(self):
with self.firewall_rule(do_delete=False, as_admin=True) as fwr, \
self.firewall_rule(name='firewall_rule2', action='reject',
do_delete=False, as_admin=True) as fwr2, \
self.firewall_rule(name='firewall_rule3', action='deny',
do_delete=False, as_admin=True) as fwr3, \
mock.patch.object(ovn_driver.OVNFwaasDriver,
'_nb_ovn') as mock_nb_ovn:
mock_nb_ovn.return_value = self.nb_ovn
fwr_id = fwr['firewall_rule']['id']
fwr2_id = fwr2['firewall_rule']['id']
fwr3_id = fwr3['firewall_rule']['id']
with self.firewall_policy(
firewall_rules=[fwr_id],
do_delete=False,
as_admin=True) as fwp:
fwp_id = fwp['firewall_policy']['id']
with self.firewall_group(
name='test',
default_policy=False,
admin_state_up=True,
ingress_firewall_policy_id=fwp_id,
egress_firewall_policy_id=fwp_id,
do_delete=False,
as_admin=True) as fwg1:
self.assertEqual(nl_constants.INACTIVE,
fwg1['firewall_group']['status'])
new_rules = [fwr_id, fwr2_id, fwr3_id]
data = {'firewall_policy': {'firewall_rules':
new_rules}}
req = (self.
new_update_request('firewall_policies', data,
fwp_id,
context=self._self_context,
as_admin=True))
res = self.deserialize(self.fmt,
req.get_response(self.ext_api))
self.assertEqual(new_rules,
res['firewall_policy']['firewall_rules'])
def test_disable_firewall_rule(self):
with self.firewall_rule(do_delete=False, as_admin=True) as fwr, \
mock.patch.object(ovn_driver.OVNFwaasDriver,
'_nb_ovn') as mock_nb_ovn:
mock_nb_ovn.return_value = self.nb_ovn
fwr_id = fwr['firewall_rule']['id']
with self.firewall_policy(
firewall_rules=[fwr_id],
do_delete=False,
as_admin=True) as fwp:
fwp_id = fwp['firewall_policy']['id']
with self.firewall_group(
name='test',
default_policy=False,
admin_state_up=True,
ingress_firewall_policy_id=fwp_id,
egress_firewall_policy_id=fwp_id,
do_delete=False,
as_admin=True) as fwg1:
self.assertEqual(nl_constants.INACTIVE,
fwg1['firewall_group']['status'])
data = {'firewall_rule': {'enabled': False}}
req = (self.
new_update_request('firewall_rules', data,
fwr_id,
context=self._self_context,
as_admin=True))
res = self.deserialize(self.fmt,
req.get_response(self.ext_api))
self.assertEqual(False,
res['firewall_rule']['enabled'])
def test_enable_firewall_rule(self):
with self.firewall_rule(enabled=False, do_delete=False,
as_admin=True) as fwr, \
mock.patch.object(ovn_driver.OVNFwaasDriver,
'_nb_ovn') as mock_nb_ovn:
mock_nb_ovn.return_value = self.nb_ovn
fwr_id = fwr['firewall_rule']['id']
with self.firewall_policy(
firewall_rules=[fwr_id],
do_delete=False,
as_admin=True) as fwp:
fwp_id = fwp['firewall_policy']['id']
with self.firewall_group(
name='test',
default_policy=False,
admin_state_up=True,
ingress_firewall_policy_id=fwp_id,
egress_firewall_policy_id=fwp_id,
do_delete=False,
as_admin=True) as fwg1:
self.assertEqual(nl_constants.INACTIVE,
fwg1['firewall_group']['status'])
data = {'firewall_rule': {'enabled': True}}
req = (self.
new_update_request('firewall_rules', data,
fwr_id,
context=self._self_context,
as_admin=True))
res = self.deserialize(self.fmt,
req.get_response(self.ext_api))
self.assertEqual(True,
res['firewall_rule']['enabled'])
def test_update_firewall_rule_with_action(self):
with self.firewall_rule(source_port=None, destination_port=None,
protocol='icmp', do_delete=False,
as_admin=True) as fwr, \
mock.patch.object(ovn_driver.OVNFwaasDriver,
'_nb_ovn') as mock_nb_ovn:
mock_nb_ovn.return_value = self.nb_ovn
fwr_id = fwr['firewall_rule']['id']
with self.firewall_policy(
firewall_rules=[fwr_id],
do_delete=False,
as_admin=True) as fwp:
fwp_id = fwp['firewall_policy']['id']
with self.firewall_group(
name='test',
default_policy=False,
admin_state_up=True,
ingress_firewall_policy_id=fwp_id,
egress_firewall_policy_id=fwp_id,
do_delete=False,
as_admin=True) as fwg1:
self.assertEqual(nl_constants.INACTIVE,
fwg1['firewall_group']['status'])
data = {'firewall_rule': {'action': 'deny'}}
req = (self.
new_update_request('firewall_rules', data,
fwr_id,
context=self._self_context,
as_admin=True))
res = self.deserialize(self.fmt,
req.get_response(self.ext_api))
self.assertEqual('deny',
res['firewall_rule']['action'])
def test_insert_rule_into_firewall_policy(self):
with self.firewall_rule(do_delete=False, as_admin=True) as fwr, \
self.firewall_rule(name='firewall_rule2', action='reject',
do_delete=False, as_admin=True) as fwr2, \
mock.patch.object(ovn_driver.OVNFwaasDriver,
'_nb_ovn') as mock_nb_ovn:
mock_nb_ovn.return_value = self.nb_ovn
fwr_id = fwr['firewall_rule']['id']
fwr2_id = fwr2['firewall_rule']['id']
with self.firewall_policy(
firewall_rules=[fwr_id],
do_delete=False,
as_admin=True) as fwp:
fwp_id = fwp['firewall_policy']['id']
with self.firewall_group(
name='test',
default_policy=False,
admin_state_up=True,
ingress_firewall_policy_id=fwp_id,
egress_firewall_policy_id=fwp_id,
do_delete=False,
as_admin=True) as fwg1:
self.assertEqual(nl_constants.INACTIVE,
fwg1['firewall_group']['status'])
data = {'firewall_rule_id': fwr2_id,
'insert_after': fwr_id}
req = (self.
new_update_request('firewall_policies', data,
fwp_id,
subresource='insert_rule',
context=self._self_context,
as_admin=True))
res = self.deserialize(self.fmt,
req.get_response(self.ext_api))
self.assertEqual([fwr_id, fwr2_id],
res['firewall_rules'])
def test_remove_rules_from_firewall_policy(self):
with self.firewall_rule(do_delete=False, as_admin=True) as fwr, \
self.firewall_rule(name='firewall_rule2', action='reject',
do_delete=False, as_admin=True) as fwr2, \
mock.patch.object(ovn_driver.OVNFwaasDriver,
'_nb_ovn') as mock_nb_ovn:
mock_nb_ovn.return_value = self.nb_ovn
fwr_id = fwr['firewall_rule']['id']
fwr2_id = fwr2['firewall_rule']['id']
with self.firewall_policy(
firewall_rules=[fwr_id, fwr2_id],
do_delete=False, as_admin=True) as fwp:
fwp_id = fwp['firewall_policy']['id']
with self.firewall_group(
name='test',
default_policy=False,
admin_state_up=True,
ingress_firewall_policy_id=fwp_id,
egress_firewall_policy_id=fwp_id,
do_delete=False,
as_admin=True) as fwg1:
self.assertEqual(nl_constants.INACTIVE,
fwg1['firewall_group']['status'])
data = {'firewall_rule_id': fwr2_id}
req = (self.
new_update_request('firewall_policies', data,
fwp_id,
subresource='remove_rule',
context=self._self_context,
as_admin=True))
res = self.deserialize(self.fmt,
req.get_response(self.ext_api))
self.assertEqual([fwr_id],
res['firewall_rules'])

View File

@ -0,0 +1,15 @@
---
features:
- L3 stateless firewall support for ML2/OVN driver is implemented.
issues:
- |
If the user configures stateful security group rules for VMs ports and
stateless L3 firewall rules for gateway ports like this:
- SG ingress rules: --remote_ip_prefix 0.0.0.0/0
- FW ingress rules: --destination_ip_address 0.0.0.0/0 --action allow
It only opens ingress traffic for another network to access VM, but the
reply traffic (egress direction) also passes because it matches the
committed conntrack entry.
So it only works well with stateless security groups for VMs.