diff --git a/neutron_fwaas/services/firewall/agents/firewall_agent_api.py b/neutron_fwaas/services/firewall/agents/firewall_agent_api.py index 68e28d7b7..261391b38 100644 --- a/neutron_fwaas/services/firewall/agents/firewall_agent_api.py +++ b/neutron_fwaas/services/firewall/agents/firewall_agent_api.py @@ -37,6 +37,10 @@ FWaaSOpts = [ 'agent_version', default=FWAAS_V1, help=_("Firewall agent class")), + cfg.StrOpt( + 'conntrack_driver', + default='conntrack', + help=_("Name of the FWaaS Conntrack Driver")), ] cfg.CONF.register_opts(FWaaSOpts, 'fwaas') diff --git a/neutron_fwaas/services/firewall/drivers/conntrack_base.py b/neutron_fwaas/services/firewall/drivers/conntrack_base.py new file mode 100644 index 000000000..23d1d8b04 --- /dev/null +++ b/neutron_fwaas/services/firewall/drivers/conntrack_base.py @@ -0,0 +1,35 @@ +# Copyright (c) 2017 Fujitsu Limited +# 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 abc + +import six + + +@six.add_metaclass(abc.ABCMeta) +class ConntrackDriverBase(object): + """Base Driver for Conntrack""" + + @abc.abstractmethod + def initialize(self, *args, **kwargs): + """Initialize the driver""" + + @abc.abstractmethod + def delete_entries(self, rules, namespace): + """Delete conntrack entries specified by list of rules""" + + @abc.abstractmethod + def flush_entries(self, namespace): + """Delete all conntrack entries within namespace""" diff --git a/neutron_fwaas/services/firewall/drivers/linux/iptables_fwaas.py b/neutron_fwaas/services/firewall/drivers/linux/iptables_fwaas.py index 61f1b2879..16cb5f333 100644 --- a/neutron_fwaas/services/firewall/drivers/linux/iptables_fwaas.py +++ b/neutron_fwaas/services/firewall/drivers/linux/iptables_fwaas.py @@ -13,10 +13,12 @@ # License for the specific language governing permissions and limitations # under the License. -from neutron.agent.linux import iptables_manager -from neutron.agent.linux import utils as linux_utils +from oslo_config import cfg from oslo_log import log as logging +from oslo_utils import excutils +from neutron.agent.linux import iptables_manager +from neutron.common import utils from neutron_fwaas._i18n import _LE from neutron_fwaas.common import fwaas_constants as f_const from neutron_fwaas.extensions import firewall as fw_ext @@ -56,6 +58,27 @@ class IptablesFwaasDriver(fwaas_base.FwaasDriverBase): def __init__(self): LOG.debug("Initializing fwaas iptables driver") self.pre_firewall = None + conntrack_cls = self._load_firewall_extension_driver( + 'neutron_fwaas.services.firewall.drivers.linux', + cfg.CONF.fwaas.conntrack_driver) + self.conntrack = conntrack_cls() + self.conntrack.initialize() + + @staticmethod + def _load_firewall_extension_driver(namespace, driver): + """Loads driver using alias or class name + :param namespace: namespace where alias is defined + :param driver: driver alias or class name + :returns driver that is loaded + :raises ImportError if fails to load driver + """ + + try: + return utils.load_class_by_alias_or_classname(namespace, + driver) + except ImportError: + with excutils.save_and_reraise_exception(): + LOG.error(_LE("Driver '%s' not found."), driver) def create_firewall(self, agent_mode, apply_list, firewall): LOG.debug('Creating firewall %(fw_id)s for tenant %(tid)s', @@ -257,26 +280,6 @@ class IptablesFwaasDriver(fwaas_base.FwaasDriverBase): def _find_new_rules(self, pre_firewall, firewall): return self._find_removed_rules(firewall, pre_firewall) - def _get_conntrack_cmd_from_rule(self, ipt_mgr, rule=None): - prefixcmd = ['ip', 'netns', 'exec'] + [ipt_mgr.namespace] - cmd = ['conntrack', '-D'] - if rule: - conntrack_filter = self._get_conntrack_filter_from_rule(rule) - exec_cmd = prefixcmd + cmd + conntrack_filter - else: - exec_cmd = prefixcmd + cmd - return exec_cmd - - def _remove_conntrack_by_cmd(self, cmd): - if cmd: - try: - linux_utils.execute(cmd, run_as_root=True, - check_exit_code=True, - extra_ok_codes=[1]) - except RuntimeError: - LOG.exception( - _LE("Failed execute conntrack command %s"), str(cmd)) - def _remove_conntrack_new_firewall(self, agent_mode, apply_list, firewall): """Remove conntrack when create new firewall""" routers_list = list(set(apply_list)) @@ -285,8 +288,7 @@ class IptablesFwaasDriver(fwaas_base.FwaasDriverBase): agent_mode, router_info) for ipt_if_prefix in ipt_if_prefix_list: ipt_mgr = ipt_if_prefix['ipt'] - cmd = self._get_conntrack_cmd_from_rule(ipt_mgr) - self._remove_conntrack_by_cmd(cmd) + self.conntrack.flush_entries(ipt_mgr.namespace) def _remove_conntrack_updated_firewall(self, agent_mode, apply_list, pre_firewall, firewall): @@ -302,27 +304,8 @@ class IptablesFwaasDriver(fwaas_base.FwaasDriverBase): i_rules = self._find_new_rules(pre_firewall, firewall) r_rules = self._find_removed_rules(pre_firewall, firewall) removed_conntrack_rules_list = ch_rules + i_rules + r_rules - for rule in removed_conntrack_rules_list: - cmd = self._get_conntrack_cmd_from_rule(ipt_mgr, rule) - self._remove_conntrack_by_cmd(cmd) - - def _get_conntrack_filter_from_rule(self, rule): - """Get conntrack filter from rule. - The key for get conntrack filter is protocol, destination_port - and source_port. If we want to take more keys, add to the list. - """ - conntrack_filter = [] - keys = [['-p', 'protocol'], ['-f', 'ip_version'], - ['--dport', 'destination_port'], ['--sport', 'source_port']] - for key in keys: - if rule.get(key[1]): - if key[1] == 'ip_version': - conntrack_filter.append(key[0]) - conntrack_filter.append('ipv' + str(rule.get(key[1]))) - else: - conntrack_filter.append(key[0]) - conntrack_filter.append(rule.get(key[1])) - return conntrack_filter + self.conntrack.delete_entries(removed_conntrack_rules_list, + ipt_mgr.namespace) def _remove_default_chains(self, nsid): """Remove fwaas default policy chain.""" diff --git a/neutron_fwaas/services/firewall/drivers/linux/legacy_conntrack.py b/neutron_fwaas/services/firewall/drivers/linux/legacy_conntrack.py new file mode 100644 index 000000000..0b6f5acff --- /dev/null +++ b/neutron_fwaas/services/firewall/drivers/linux/legacy_conntrack.py @@ -0,0 +1,81 @@ +# Copyright (c) 2017 Fujitsu Limited +# 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 oslo_log import log as logging + +from neutron.agent.linux import utils as linux_utils + +from neutron_fwaas._i18n import _ +from neutron_fwaas.services.firewall.drivers import conntrack_base + + +LOG = logging.getLogger(__name__) + + +class ConntrackLegacy(conntrack_base.ConntrackDriverBase): + def initialize(self, execute=None): + LOG.debug('Initialize Conntrack Legacy') + self.execute = execute or linux_utils.execute + + def flush_entries(self, namespace): + prefixcmd = ['ip', 'netns', 'exec', namespace] if namespace else [] + cmd = prefixcmd + ['conntrack', '-D'] + self._execute_command(cmd) + + def delete_entries(self, rules, namespace): + for rule in rules: + cmd = self._get_conntrack_cmd_from_rule(rule, namespace) + self._execute_command(cmd) + + def _execute_command(self, cmd): + try: + self.execute(cmd, + run_as_root=True, + check_exit_code=True, + extra_ok_codes=[1]) + except RuntimeError: + msg = _("Failed execute conntrack command %s") % cmd + raise RuntimeError(msg) + + def _get_conntrack_cmd_from_rule(self, rule, namespace): + prefixcmd = (['ip', 'netns', 'exec', namespace] + if namespace else []) + cmd = ['conntrack', '-D'] + if rule: + conntrack_filter = self._get_conntrack_filter_from_rule(rule) + exec_cmd = prefixcmd + cmd + conntrack_filter + else: + exec_cmd = prefixcmd + cmd + return exec_cmd + + def _get_conntrack_filter_from_rule(self, rule): + """Get conntrack filter from rule + + The key for get conntrack filter is protocol, destination_port + and source_port. If we want to take more keys, add to the list. + """ + conntrack_filter = [] + keys = [['-p', 'protocol'], ['-f', 'ip_version'], + ['--dport', 'destination_port'], ['--sport', 'source_port']] + for arg_key, rule_key in keys: + val = rule.get(rule_key) + if val: + if rule_key == 'ip_version': + conntrack_filter.append(arg_key) + conntrack_filter.append('ipv' + str(val)) + else: + conntrack_filter.append(arg_key) + conntrack_filter.append(val) + return conntrack_filter diff --git a/neutron_fwaas/tests/unit/services/firewall/drivers/linux/test_iptables_fwaas.py b/neutron_fwaas/tests/unit/services/firewall/drivers/linux/test_iptables_fwaas.py index 5f0404172..22c72733c 100644 --- a/neutron_fwaas/tests/unit/services/firewall/drivers/linux/test_iptables_fwaas.py +++ b/neutron_fwaas/tests/unit/services/firewall/drivers/linux/test_iptables_fwaas.py @@ -34,13 +34,15 @@ FW_LEGACY = 'legacy' class IptablesFwaasTestCase(base.BaseTestCase): def setUp(self): super(IptablesFwaasTestCase, self).setUp() - self.utils_exec_p = mock.patch( - 'neutron.agent.linux.utils.execute') - self.utils_exec = self.utils_exec_p.start() + self.conntrack_driver = mock.Mock() + self.conntrack_driver.initialize = mock.Mock() + self.conntrack_driver.delete_entries = mock.Mock() + self.conntrack_driver.flush_entries = mock.Mock() self.iptables_cls_p = mock.patch( 'neutron.agent.linux.iptables_manager.IptablesManager') self.iptables_cls_p.start() self.firewall = fwaas.IptablesFwaasDriver() + self.firewall.conntrack = self.conntrack_driver def _fake_rules_v4(self, fwid, apply_list): rule_list = [] @@ -285,11 +287,8 @@ class IptablesFwaasTestCase(base.BaseTestCase): self.firewall.create_firewall(FW_LEGACY, apply_list, firewall) for router_info_inst in apply_list: namespace = router_info_inst.iptables_manager.namespace - cmd = ['ip', 'netns', 'exec', namespace, 'conntrack', '-D'] - calls = [ - mock.call(cmd, run_as_root=True, check_exit_code=True, - extra_ok_codes=[1])] - self.utils_exec.assert_has_calls(calls) + calls = [mock.call(namespace)] + self.conntrack_driver.flush_entries.assert_has_calls(calls) def test_remove_conntrack_inserted_rule(self): apply_list = self._fake_apply_list() @@ -305,18 +304,35 @@ class IptablesFwaasTestCase(base.BaseTestCase): rule_list.insert(2, insert_rule) firewall = self._fake_firewall(rule_list) self.firewall.update_firewall(FW_LEGACY, apply_list, firewall) + rules_changed = [ + {'destination_port': '23', + 'position': '2', + 'protocol': 'tcp', + 'ip_version': 4, + 'enabled': True, + 'action': 'reject', + 'id': 'fake-fw-rule3'}, + {'destination_port': '23', + 'position': '3', + 'protocol': 'tcp', + 'ip_version': 4, + 'enabled': True, + 'action': 'reject', + 'id': 'fake-fw-rule3'} + ] + rules_inserted = [ + {'id': 'fake-fw-rule', + 'protocol': 'icmp', + 'ip_version': 4, + 'enabled': True, + 'action': 'deny', + 'position': '2'} + ] for router_info_inst in apply_list: namespace = router_info_inst.iptables_manager.namespace - cmd1 = ['ip', 'netns', 'exec', namespace, 'conntrack', - '-D', '-p', 'tcp', '-f', 'ipv4', '--dport', '23'] - cmd2 = ['ip', 'netns', 'exec', namespace, 'conntrack', - '-D', '-p', 'icmp', '-f', 'ipv4'] - calls = [ - mock.call(cmd1, run_as_root=True, check_exit_code=True, - extra_ok_codes=[1]), - mock.call(cmd2, run_as_root=True, check_exit_code=True, - extra_ok_codes=[1])] - self.utils_exec.assert_has_calls(calls) + self.conntrack_driver.delete_entries.assert_called_once_with( + rules_changed + rules_inserted, namespace + ) def test_remove_conntrack_removed_rule(self): apply_list = self._fake_apply_list() @@ -328,18 +344,36 @@ class IptablesFwaasTestCase(base.BaseTestCase): rule_list.remove(remove_rule) firewall = self._fake_firewall(rule_list) self.firewall.update_firewall(FW_LEGACY, apply_list, firewall) + rules_changed = [ + {'destination_port': '23', + 'position': '2', + 'protocol': 'tcp', + 'ip_version': 4, + 'enabled': True, + 'action': 'reject', + 'id': 'fake-fw-rule3'}, + {'destination_port': '23', + 'position': '1', + 'protocol': 'tcp', + 'ip_version': 4, + 'enabled': True, + 'action': 'reject', + 'id': 'fake-fw-rule3'} + ] + rules_removed = [ + {'enabled': True, + 'position': '1', + 'protocol': 'tcp', + 'id': 'fake-fw-rule2', + 'ip_version': 4, + 'action': 'deny', + 'destination_port': '22'} + ] for router_info_inst in apply_list: namespace = router_info_inst.iptables_manager.namespace - cmd1 = ['ip', 'netns', 'exec', namespace, 'conntrack', - '-D', '-p', 'tcp', '-f', 'ipv4', '--dport', '23'] - cmd2 = ['ip', 'netns', 'exec', namespace, 'conntrack', - '-D', '-p', 'tcp', '-f', 'ipv4', '--dport', '22'] - calls = [ - mock.call(cmd1, run_as_root=True, check_exit_code=True, - extra_ok_codes=[1]), - mock.call(cmd2, run_as_root=True, check_exit_code=True, - extra_ok_codes=[1])] - self.utils_exec.assert_has_calls(calls) + self.conntrack_driver.delete_entries.assert_called_once_with( + rules_changed + rules_removed, namespace + ) def test_remove_conntrack_changed_rule(self): apply_list = self._fake_apply_list() @@ -349,20 +383,28 @@ class IptablesFwaasTestCase(base.BaseTestCase): income_rule = {'enabled': True, 'action': 'deny', 'ip_version': 4, - 'protocol': 'icmp', - 'id': 'fake-fw-rule2'} - rule_list[1] = income_rule + 'protocol': 'tcp', + 'id': 'fake-fw-rule3'} + rule_list[2] = income_rule firewall = self._fake_firewall(rule_list) self.firewall.update_firewall(FW_LEGACY, apply_list, firewall) + rules_changed = [ + {'id': 'fake-fw-rule3', + 'enabled': True, + 'action': 'reject', + 'position': '2', + 'destination_port': '23', + 'ip_version': 4, + 'protocol': 'tcp'}, + {'position': '2', + 'enabled': True, + 'action': 'deny', + 'id': 'fake-fw-rule3', + 'ip_version': 4, + 'protocol': 'tcp'} + ] for router_info_inst in apply_list: namespace = router_info_inst.iptables_manager.namespace - cmd1 = ['ip', 'netns', 'exec', namespace, 'conntrack', '-D', - '-p', 'tcp', '-f', 'ipv4', '--dport', '22'] - cmd2 = ['ip', 'netns', 'exec', namespace, 'conntrack', '-D', - '-p', 'icmp', '-f', 'ipv4'] - calls = [ - mock.call(cmd1, run_as_root=True, check_exit_code=True, - extra_ok_codes=[1]), - mock.call(cmd2, run_as_root=True, check_exit_code=True, - extra_ok_codes=[1])] - self.utils_exec.assert_has_calls(calls) + self.conntrack_driver.delete_entries.assert_called_once_with( + rules_changed, namespace + ) diff --git a/neutron_fwaas/tests/unit/services/firewall/drivers/linux/test_legacy_conntrack.py b/neutron_fwaas/tests/unit/services/firewall/drivers/linux/test_legacy_conntrack.py new file mode 100644 index 000000000..510f9052e --- /dev/null +++ b/neutron_fwaas/tests/unit/services/firewall/drivers/linux/test_legacy_conntrack.py @@ -0,0 +1,94 @@ +# Copyright (c) 2017 Fujitsu Limited +# 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 mock +import testtools + +from neutron.tests import base +from neutron_fwaas.services.firewall.drivers.linux import legacy_conntrack + + +FW_RULES = [ + {'position': '2', + 'protocol': 'icmp', + 'ip_version': 4, + 'enabled': True, + 'action': 'reject', + 'id': 'fake-fw-rule1'}, + {'source_port': '1', + 'destination_port': '2', + 'position': '2', + 'protocol': 'tcp', + 'ip_version': 4, + 'enabled': True, + 'action': 'reject', + 'id': 'fake-fw-rule2'}, + {'source_port': '1', + 'destination_port': '2', + 'position': '3', + 'protocol': 'udp', + 'ip_version': 4, + 'enabled': True, + 'action': 'reject', + 'id': 'fake-fw-rule3'}, +] + +ROUTER_NAMESPACE = 'qrouter-fake-namespace' + + +class ConntrackLegacyTestCase(base.BaseTestCase): + def setUp(self): + super(ConntrackLegacyTestCase, self).setUp() + self.utils_exec = mock.Mock() + self.conntrack_driver = legacy_conntrack.ConntrackLegacy() + self.conntrack_driver.initialize(execute=self.utils_exec) + + def test_excecute_command_failed(self): + with testtools.ExpectedException(RuntimeError): + self.conntrack_driver._execute_command(['fake', 'command']) + raise RuntimeError("Failed execute conntrack command fake command") + + def test_flush_entries(self): + self.conntrack_driver.flush_entries(ROUTER_NAMESPACE) + self.utils_exec.assert_called_with( + ['ip', 'netns', 'exec', ROUTER_NAMESPACE, + 'conntrack', '-D'], + check_exit_code=True, + extra_ok_codes=[1], + run_as_root=True) + + def test_delete_entries(self): + self.conntrack_driver.delete_entries(FW_RULES, ROUTER_NAMESPACE) + calls = [ + mock.call(['ip', 'netns', 'exec', ROUTER_NAMESPACE, + 'conntrack', '-D', '-p', 'icmp', '-f', 'ipv4'], + check_exit_code=True, + extra_ok_codes=[1], + run_as_root=True), + mock.call(['ip', 'netns', 'exec', ROUTER_NAMESPACE, + 'conntrack', '-D', '-p', 'tcp', '-f', 'ipv4', + '--dport', '2', '--sport', '1'], + check_exit_code=True, + extra_ok_codes=[1], + run_as_root=True), + mock.call(['ip', 'netns', 'exec', ROUTER_NAMESPACE, + 'conntrack', '-D', '-p', 'udp', '-f', 'ipv4', + '--dport', '2', '--sport', '1'], + check_exit_code=True, + extra_ok_codes=[1], + run_as_root=True), + + ] + self.utils_exec.assert_has_calls(calls) diff --git a/setup.cfg b/setup.cfg index e352fadf2..e8cb54d37 100644 --- a/setup.cfg +++ b/setup.cfg @@ -51,6 +51,8 @@ oslo.config.opts = neutron.agent.l3.extensions = fwaas = neutron_fwaas.services.firewall.agents.l3reference.firewall_l3_agent:L3WithFWaaS fwaas_v2 = neutron_fwaas.services.firewall.agents.l3reference.firewall_l3_agent_v2:L3WithFWaaS +neutron_fwaas.services.firewall.drivers.linux = + conntrack = neutron_fwaas.services.firewall.drivers.linux.legacy_conntrack:ConntrackLegacy [build_sphinx] all_files = 1