ConntrackNetlink driver to delete conntrack entries
This patch proposes ConntrackNetlink driver to delete conntrack entries. Using Netlink will save about 90 percent of time that used by conntrack-tools. For detail information, visit: https://goo.gl/3tm9Fx Partial-Bug: #1664294 Change-Id: Id9a3b7c3f8fedbd91ced1a5b359dbf568cd26653 Co-Authored-By: Nguyen Phuong An <AnNP@vn.fujitsu.com>
This commit is contained in:
parent
5f91af2754
commit
ec9d37183a
@ -0,0 +1,137 @@
|
||||
# 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_fwaas.privileged import netlink_lib as nl_lib
|
||||
from neutron_fwaas.services.firewall.drivers import conntrack_base
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConntrackNetlink(conntrack_base.ConntrackDriverBase):
|
||||
def initialize(self, *args, **kwargs):
|
||||
LOG.debug('Conntrack Netlink loaded')
|
||||
|
||||
def flush_entries(self, namespace):
|
||||
"""Flush all conntrack entries within the namespace
|
||||
|
||||
:param namespace: namespace to flush
|
||||
:return: None
|
||||
"""
|
||||
nl_lib.flush_entries(namespace)
|
||||
|
||||
def delete_entries(self, rules, namespace):
|
||||
rule_filters = (self._get_filter_from_rule(r) for r in rules)
|
||||
rule_filters = sorted(rule_filters)
|
||||
entries = nl_lib.list_entries(namespace)
|
||||
delete_entries = self._get_entries_to_delete(rule_filters, entries)
|
||||
if delete_entries:
|
||||
nl_lib.delete_entries(delete_entries, namespace)
|
||||
|
||||
def _get_entries_to_delete(self, rule_filters, entries):
|
||||
"""Specify conntrack entries to delete
|
||||
|
||||
:param rule_filters: List of filters parsed from firewall rules
|
||||
:param entries: all entries within namespace
|
||||
:return: conntrack entries to delete
|
||||
"""
|
||||
# List all entries from namespace, they are already parsed
|
||||
# to a list of tuples:
|
||||
# [(4, 'icmp', 8, 0, '1.1.1.1', '2.2.2.2', 1234),
|
||||
# (4, 'tcp', 1, 2, '1.1.1.1', '2.2.2.2')]
|
||||
delete_entries = []
|
||||
entry_index = 0
|
||||
entry_number = len(entries)
|
||||
for rule_filter in rule_filters:
|
||||
while entry_index < entry_number:
|
||||
# Compare entry with rule
|
||||
comp = self._compare_entry_and_rule(rule_filter,
|
||||
entries[entry_index])
|
||||
# Increase entry_index when entry is under rule
|
||||
if comp < 0:
|
||||
entry_index += 1
|
||||
# Append entry to delete_entry if it matches with rule
|
||||
elif comp == 0:
|
||||
delete_entries.append(entries[entry_index])
|
||||
entry_index += 1
|
||||
# Switch to new higher rule
|
||||
else:
|
||||
break
|
||||
return delete_entries
|
||||
|
||||
@staticmethod
|
||||
def _get_filter_from_rule(rule):
|
||||
"""Parse the firewall rule to a tuple
|
||||
|
||||
:param rule: firewall rule
|
||||
:return: a tuple of parsed information
|
||||
"""
|
||||
rule_filter = []
|
||||
keys = ['ip_version', 'protocol',
|
||||
'source_port', 'destination_port',
|
||||
'source_ip_address', 'destination_ip_address']
|
||||
for key in keys:
|
||||
if key in ['source_port', 'destination_port']:
|
||||
port_range = rule.get(key, [])
|
||||
if port_range:
|
||||
port_lower, sep, port_upper = port_range.partition(':')
|
||||
port_upper = port_upper if sep else port_lower
|
||||
port_range = [port_lower, port_upper]
|
||||
rule_filter.append(port_range or [])
|
||||
else:
|
||||
rule_filter.append(rule.get(key, []))
|
||||
return tuple(rule_filter)
|
||||
|
||||
@staticmethod
|
||||
def _compare_entry_and_rule(rule_filter, entry):
|
||||
"""Define that the entry should be deleted or not
|
||||
|
||||
:param rule_filter: filter that is parsed from a firewall rule
|
||||
ex: (4, 'tcp', 1, 2)
|
||||
:param entry: parsed conntrack entry,
|
||||
ex: (4, 'tcp', 1, 2, '1.1.1.1', '2.2.2.2')
|
||||
:return: -1 if entry is lower than rule, 0 if entry matches rule,
|
||||
1 if entry is higher than rule
|
||||
"""
|
||||
ENTRY_IS_LOWER = -1
|
||||
ENTRY_MATCHES = 0
|
||||
ENTRY_IS_HIGHER = 1
|
||||
rule_ipversion = rule_filter[0]
|
||||
if entry[0] < rule_ipversion:
|
||||
return ENTRY_IS_LOWER
|
||||
elif entry[0] > rule_ipversion:
|
||||
return ENTRY_IS_HIGHER
|
||||
rule_protocol = rule_filter[1]
|
||||
if rule_protocol:
|
||||
if entry[1] < rule_protocol:
|
||||
return ENTRY_IS_LOWER
|
||||
elif entry[1] > rule_protocol:
|
||||
return ENTRY_IS_HIGHER
|
||||
sport_range = rule_filter[2]
|
||||
if sport_range:
|
||||
sport_range = [int(port) for port in sport_range]
|
||||
if entry[2] < min(sport_range[0], sport_range[-1]):
|
||||
return ENTRY_IS_LOWER
|
||||
elif entry[2] > max(sport_range[0], sport_range[-1]):
|
||||
return ENTRY_IS_HIGHER
|
||||
dport_range = rule_filter[3]
|
||||
if dport_range:
|
||||
dport_range = [int(port) for port in dport_range]
|
||||
if entry[3] < min(dport_range[0], dport_range[-1]):
|
||||
return ENTRY_IS_LOWER
|
||||
elif entry[3] > max(dport_range[0], dport_range[-1]):
|
||||
return ENTRY_IS_HIGHER
|
||||
return ENTRY_MATCHES
|
@ -0,0 +1,239 @@
|
||||
# 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
|
||||
|
||||
from neutron_fwaas.services.firewall.drivers.linux import netlink_conntrack
|
||||
from neutron_fwaas.tests import base
|
||||
|
||||
FW_RULES = [
|
||||
{'position': '1',
|
||||
'protocol': 'icmp',
|
||||
'ip_version': 4,
|
||||
'enabled': True,
|
||||
'action': 'reject',
|
||||
'id': 'fake-fw-rule1'},
|
||||
{'source_port': '0:10',
|
||||
'destination_port': '0:10',
|
||||
'position': '2',
|
||||
'protocol': 'tcp',
|
||||
'ip_version': 4,
|
||||
'enabled': True,
|
||||
'action': 'reject',
|
||||
'id': 'fake-fw-rule2'},
|
||||
{'source_port': '0:10',
|
||||
'destination_port': '0:20',
|
||||
'position': '3',
|
||||
'protocol': 'udp',
|
||||
'ip_version': 4,
|
||||
'enabled': True,
|
||||
'action': 'reject',
|
||||
'id': 'fake-fw-rule3'},
|
||||
{'source_port': None,
|
||||
'destination_port': '0:10',
|
||||
'position': '2',
|
||||
'protocol': 'tcp',
|
||||
'ip_version': 4,
|
||||
'enabled': True,
|
||||
'action': 'reject',
|
||||
'id': 'fake-fw-rule5'},
|
||||
{'source_port': '0:10',
|
||||
'destination_port': None,
|
||||
'position': '3',
|
||||
'protocol': 'udp',
|
||||
'ip_version': 4,
|
||||
'enabled': True,
|
||||
'action': 'reject',
|
||||
'id': 'fake-fw-rule5'},
|
||||
]
|
||||
|
||||
ICMP_ENTRY = (4, 'icmp', 8, 0, '1.1.1.1', '2.2.2.2', '1234')
|
||||
TCP_ENTRY = (4, 'tcp', 1, 2, '1.1.1.1', '2.2.2.2')
|
||||
UDP_ENTRY = (4, 'udp', 1, 2, '1.1.1.1', '2.2.2.2')
|
||||
|
||||
ROUTER_NAMESPACE = 'qrouter-fake-namespace'
|
||||
|
||||
|
||||
class ConntrackNetlinkTestCase(base.BaseTestCase):
|
||||
def setUp(self):
|
||||
super(ConntrackNetlinkTestCase, self).setUp()
|
||||
self.conntrack_driver = netlink_conntrack.ConntrackNetlink()
|
||||
self.conntrack_driver.initialize()
|
||||
nl_flush_entries = mock.patch('neutron_fwaas.privileged.'
|
||||
'netlink_lib.flush_entries')
|
||||
self.flush_entries = nl_flush_entries.start()
|
||||
nl_list_entries = mock.patch('neutron_fwaas.privileged.'
|
||||
'netlink_lib.list_entries')
|
||||
self.list_entries = nl_list_entries.start()
|
||||
nl_delete_entries = mock.patch('neutron_fwaas.privileged.'
|
||||
'netlink_lib.delete_entries')
|
||||
self.delete_entries = nl_delete_entries.start()
|
||||
|
||||
def test_flush_entries(self):
|
||||
self.conntrack_driver.flush_entries(ROUTER_NAMESPACE)
|
||||
self.flush_entries.assert_called_with(ROUTER_NAMESPACE)
|
||||
|
||||
def test_delete_with_empty_conntrack_entries(self):
|
||||
self.list_entries.return_value = []
|
||||
self.conntrack_driver.delete_entries([], ROUTER_NAMESPACE)
|
||||
self.list_entries.assert_called_with(ROUTER_NAMESPACE)
|
||||
self.delete_entries.assert_not_called()
|
||||
|
||||
def test_delete_icmp_entry(self):
|
||||
"""Testing delete an icmp entry
|
||||
|
||||
The icmp entry can be deleted if there is an icmp conntrack entry
|
||||
matched with an icmp firewall rule.
|
||||
The information passing to nl_lib.kill_entry will include:
|
||||
(ipversion, protocol, icmp_type, icmp_code, src_address, dst_addres,
|
||||
icmp_ip)
|
||||
"""
|
||||
self.list_entries.return_value = [ICMP_ENTRY]
|
||||
self.conntrack_driver.delete_entries(FW_RULES, ROUTER_NAMESPACE)
|
||||
self.list_entries.assert_called_with(ROUTER_NAMESPACE)
|
||||
self.delete_entries.assert_called_with([(4, 'icmp', 8, 0,
|
||||
'1.1.1.1', '2.2.2.2',
|
||||
'1234')], ROUTER_NAMESPACE)
|
||||
|
||||
def test_delete_tcp_entry(self):
|
||||
"""Testing delete a tcp entry
|
||||
|
||||
The tcp entry can be deleted if there is a tcp conntrack entry
|
||||
matched with a tcp firewall rule.
|
||||
The information passing to nl_lib.kill_entry will include:
|
||||
(ipversion, protocol, src_port, dst_port, src_address, dst_addres)
|
||||
"""
|
||||
self.list_entries.return_value = [TCP_ENTRY]
|
||||
self.conntrack_driver.delete_entries(FW_RULES, ROUTER_NAMESPACE)
|
||||
self.list_entries.assert_called_with(ROUTER_NAMESPACE)
|
||||
self.delete_entries.assert_called_with(
|
||||
[(4, 'tcp', 1, 2, '1.1.1.1', '2.2.2.2')], ROUTER_NAMESPACE)
|
||||
|
||||
def test_delete_udp_entry(self):
|
||||
"""Testing delete an udp entry
|
||||
|
||||
The udp entry can be deleted if there is an udp conntrack entry
|
||||
matched with an udp firewall rule.
|
||||
The information passing to nl_lib.kill_entry will include:
|
||||
(ipversion, protocol, src_port, dst_port, src_address, dst_addres)
|
||||
"""
|
||||
self.list_entries.return_value = [UDP_ENTRY]
|
||||
self.conntrack_driver.delete_entries(FW_RULES, ROUTER_NAMESPACE)
|
||||
self.list_entries.assert_called_with(ROUTER_NAMESPACE)
|
||||
self.delete_entries.assert_called_with(
|
||||
[(4, 'udp', 1, 2, '1.1.1.1', '2.2.2.2')], ROUTER_NAMESPACE)
|
||||
|
||||
def test_delete_multiple_entries(self):
|
||||
self.list_entries.return_value = [ICMP_ENTRY, TCP_ENTRY, UDP_ENTRY]
|
||||
self.conntrack_driver.delete_entries(FW_RULES, ROUTER_NAMESPACE)
|
||||
self.list_entries.assert_called_with(ROUTER_NAMESPACE)
|
||||
self.delete_entries.assert_called_with(
|
||||
[(4, 'icmp', 8, 0, '1.1.1.1', '2.2.2.2', '1234'),
|
||||
(4, 'tcp', 1, 2, '1.1.1.1', '2.2.2.2'),
|
||||
(4, 'udp', 1, 2, '1.1.1.1', '2.2.2.2')], ROUTER_NAMESPACE)
|
||||
|
||||
def _test_entry_to_delete(self, rule_filter, entry, expect_result):
|
||||
is_entry_to_delete = (
|
||||
self.conntrack_driver._compare_entry_and_rule(rule_filter, entry))
|
||||
self.assertEqual(expect_result, is_entry_to_delete)
|
||||
|
||||
def test_icmp_entry_match_rule(self):
|
||||
entry = (4, 'icmp', 8, 0, '1.1.1.1', '2.2.2.2', '1234')
|
||||
rule_filter = (4, 'icmp', None, None)
|
||||
self._test_entry_to_delete(rule_filter, entry, 0)
|
||||
|
||||
def test_tcp_entry_match_rule(self):
|
||||
entry = (4, 'tcp', 1, 2, '1.1.1.1', '2.2.2.2')
|
||||
rule_filters = [(4, 'tcp', None, None),
|
||||
(4, 'tcp', [1], None),
|
||||
(4, 'tcp', None, [2]),
|
||||
(4, 'tcp', [1], [2]),
|
||||
(4, 'tcp', ['0', '10'], ['0', '10']), ]
|
||||
for rule_filter in rule_filters:
|
||||
self._test_entry_to_delete(rule_filter, entry, 0)
|
||||
|
||||
def test_udp_entry_match_rule(self):
|
||||
entry = (4, 'udp', 1, 2, '1.1.1.1', '2.2.2.2')
|
||||
rule_filters = [(4, 'udp', None, None),
|
||||
(4, 'udp', [1], None),
|
||||
(4, 'udp', None, [2]),
|
||||
(4, 'udp', [1], [2]),
|
||||
(4, 'udp', ['0', '10'], ['0', '10']), ]
|
||||
for rule_filter in rule_filters:
|
||||
self._test_entry_to_delete(rule_filter, entry, 0)
|
||||
|
||||
def test_entry_unmatch_rule(self):
|
||||
wrong_ipv = [(4, 'tcp', '1', '2', '1.1.1.1', '2.2.2.2'),
|
||||
(6, 'tcp', None, None), -1]
|
||||
wrong_proto = [(4, 'tcp', '1', '2', '1.1.1.1', '2.2.2.2'),
|
||||
(4, 'udp', None, None), -1]
|
||||
not_in_sport_range = [(4, 'tcp', 1, 2, '1.1.1.1', '2.2.2.2'),
|
||||
(4, 'tcp', ['2', '100'], [2]), -1]
|
||||
not_in_dport_range = [(4, 'tcp', 1, 2, '1.1.1.1', '2.2.2.2'),
|
||||
(4, 'tcp', [1], ['3', '100']), -1]
|
||||
for entry, rule_filter, expect in [
|
||||
wrong_ipv, wrong_proto, not_in_sport_range, not_in_dport_range]:
|
||||
self._test_entry_to_delete(rule_filter, entry, expect)
|
||||
|
||||
def test_get_filter_from_rules(self):
|
||||
fw_rule_icmp = FW_RULES[0]
|
||||
fw_rule_port_range = FW_RULES[1]
|
||||
fw_rule_dest_port = FW_RULES[3]
|
||||
fw_rule_source_port = FW_RULES[4]
|
||||
|
||||
# filter format:
|
||||
# ('ip_version', 'protocol', 'source_port', 'destination_port',
|
||||
# 'source_ip_address', 'destination_ip_address')
|
||||
|
||||
expected_icmp_filter = (4, 'icmp', [], [], [], [])
|
||||
expected_port_range_filter = (4, 'tcp', ['0', '10'], ['0', '10'],
|
||||
[], [])
|
||||
expected_dest_port_filter = (4, 'tcp', [], ['0', '10'], [], [])
|
||||
expected_source_port_filter = (4, 'udp', ['0', '10'], [], [], [])
|
||||
|
||||
actual_icmp_filter = self.conntrack_driver._get_filter_from_rule(
|
||||
fw_rule_icmp)
|
||||
actual_port_range_filter = \
|
||||
self.conntrack_driver._get_filter_from_rule(fw_rule_port_range)
|
||||
actual_dest_port_filter = \
|
||||
self.conntrack_driver._get_filter_from_rule(fw_rule_dest_port)
|
||||
actual_source_port_filter = \
|
||||
self.conntrack_driver._get_filter_from_rule(fw_rule_source_port)
|
||||
|
||||
self.assertEqual(expected_icmp_filter, actual_icmp_filter)
|
||||
self.assertEqual(expected_port_range_filter, actual_port_range_filter)
|
||||
self.assertEqual(expected_dest_port_filter, actual_dest_port_filter)
|
||||
self.assertEqual(expected_source_port_filter,
|
||||
actual_source_port_filter)
|
||||
|
||||
def test_get_entries_to_delete(self):
|
||||
rule_filters = sorted(
|
||||
[(4, 'tcp', ['0', '10'], ['1', '10']),
|
||||
(4, 'udp', ['0', '10'], ['0', '10']),
|
||||
(4, 'icmp', None, None)])
|
||||
TCP_ENTRY_IN_RANGE = (4, 'tcp', 2, 3, '1.1.1.1', '2.2.2.2')
|
||||
TCP_ENTRY_OUT_RANGE = (4, 'tcp', 22, 100, '1.1.1.1', '2.2.2.2')
|
||||
UDP_ENTRY_IN_RANGE = (4, 'udp', 3, 4, '1.1.1.1', '2.2.2.2')
|
||||
UDP_ENTRY_OUT_RANGE = (4, 'udp', 100, 200, '1.1.1.1', '2.2.2.2')
|
||||
self.list_entries.return_value = sorted(
|
||||
[ICMP_ENTRY, TCP_ENTRY, UDP_ENTRY,
|
||||
TCP_ENTRY_IN_RANGE, TCP_ENTRY_OUT_RANGE,
|
||||
UDP_ENTRY_IN_RANGE, UDP_ENTRY_OUT_RANGE])
|
||||
expected_delete_entries = sorted(
|
||||
[ICMP_ENTRY, TCP_ENTRY, UDP_ENTRY,
|
||||
TCP_ENTRY_IN_RANGE, UDP_ENTRY_IN_RANGE])
|
||||
actual_delete_entries = self.conntrack_driver._get_entries_to_delete(
|
||||
rule_filters, self.list_entries())
|
||||
self.assertEqual(expected_delete_entries, actual_delete_entries)
|
@ -53,6 +53,7 @@ neutron.agent.l3.extensions =
|
||||
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
|
||||
netlink_conntrack = neutron_fwaas.services.firewall.drivers.linux.netlink_conntrack:ConntrackNetlink
|
||||
|
||||
[build_sphinx]
|
||||
all_files = 1
|
||||
|
Loading…
Reference in New Issue
Block a user