Enable to configure conntrack driver

This patch enables to configure conntrack driver.
Initially, "conntrack-tools" is being used to manage connection,
however, it's costly and down performance[1]. The alternative can be
found here[2] with need to improve reliability and stability.

[1] https://bugs.launchpad.net/neutron/+bug/1630832
[2] https://review.openstack.org/#/c/389654/

Partial-Bug: #1664294
Co-Authored-By: Nguyen Phuong An <AnNP@vn.fujitsu.com>
Change-Id: Id0597f74bef67b85776445e7bc591eb085f55acc
This commit is contained in:
Cuong Nguyen 2017-02-14 19:21:24 +07:00
parent 2074e1a3d5
commit 8f6a1b90af
7 changed files with 327 additions and 86 deletions

View File

@ -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')

View File

@ -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"""

View File

@ -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."""

View File

@ -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

View File

@ -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
)

View File

@ -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)

View File

@ -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