From 1e5432cccdb29101b6f5177a6a98b23000e86ece Mon Sep 17 00:00:00 2001 From: Cuong Nguyen Date: Thu, 15 Jun 2017 16:24:47 +0700 Subject: [PATCH] Add netlink-lib to manage conntrack entries When the security group is updated, the conntrack entries will be deleted by conntrack-tools with each rule associated with each SG rule. In large scale system, updating so much rules will call a large number of subprocesses to implement the "conntrack -D" commands. That will consume the system resource and time. This netlink-lib will be used by netlink conntrack driver to improve conntrack management performance. Original solution and performance from neutron-fwaas [1] [1] https://review.openstack.org/#/c/438445/ Co-Authored-By: Cao Xuan Hoang Change-Id: I7503c87900eb0f7bc5386f915b925bb2576502cc --- .../agent/linux/netlink_constants.py | 91 +++++ neutron/privileged/agent/linux/netlink_lib.py | 275 ++++++++++++++ .../agent/linux/test_netlink_lib.py | 110 ++++++ neutron/tests/unit/privileged/__init__.py | 0 .../tests/unit/privileged/agent/__init__.py | 0 .../unit/privileged/agent/linux/__init__.py | 0 .../agent/linux/test_netlink_lib.py | 342 ++++++++++++++++++ 7 files changed, 818 insertions(+) create mode 100644 neutron/privileged/agent/linux/netlink_constants.py create mode 100644 neutron/privileged/agent/linux/netlink_lib.py create mode 100644 neutron/tests/functional/agent/linux/test_netlink_lib.py create mode 100644 neutron/tests/unit/privileged/__init__.py create mode 100644 neutron/tests/unit/privileged/agent/__init__.py create mode 100644 neutron/tests/unit/privileged/agent/linux/__init__.py create mode 100644 neutron/tests/unit/privileged/agent/linux/test_netlink_lib.py diff --git a/neutron/privileged/agent/linux/netlink_constants.py b/neutron/privileged/agent/linux/netlink_constants.py new file mode 100644 index 00000000000..af53336dec0 --- /dev/null +++ b/neutron/privileged/agent/linux/netlink_constants.py @@ -0,0 +1,91 @@ +# 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. +# +# Some parts are based on python-conntrack: +# Copyright (c) 2009-2011,2015 Andrew Grigorev +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +import socket + + +CONNTRACK = 0 + +NFCT_O_PLAIN = 0 + +NFCT_OF_TIME_BIT = 1 +NFCT_OF_TIME = 1 << NFCT_OF_TIME_BIT + +NFCT_Q_DESTROY = 2 +NFCT_Q_FLUSH = 4 +NFCT_Q_DUMP = 5 +NFCT_T_DESTROY_BIT = 2 +NFCT_T_DESTROY = 1 << NFCT_T_DESTROY_BIT + +ATTR_IPV4_SRC = 0 +ATTR_IPV4_DST = 1 +ATTR_IPV6_SRC = 4 +ATTR_IPV6_DST = 5 +ATTR_PORT_SRC = 8 +ATTR_PORT_DST = 9 +ATTR_ICMP_TYPE = 12 +ATTR_ICMP_CODE = 13 +ATTR_ICMP_ID = 14 +ATTR_L3PROTO = 15 +ATTR_L4PROTO = 17 +ATTR_ZONE = 61 + +NFCT_T_NEW_BIT = 0 +NFCT_T_NEW = 1 << NFCT_T_NEW_BIT +NFCT_T_UPDATE_BIT = 1 +NFCT_T_UPDATE = 1 << NFCT_T_UPDATE_BIT +NFCT_T_DESTROY_BIT = 2 +NFCT_T_DESTROY = 1 << NFCT_T_DESTROY_BIT + +NFCT_T_ALL = NFCT_T_NEW | NFCT_T_UPDATE | NFCT_T_DESTROY + +NFCT_CB_CONTINUE = 1 +NFCT_CB_FAILURE = -1 + +NFNL_SUBSYS_CTNETLINK = 0 + +BUFFER = 1024 +# IPv6 address memory buffer +ADDR_BUFFER_6 = 16 +ADDR_BUFFER_4 = 4 + +IPVERSION_SOCKET = {4: socket.AF_INET, 6: socket.AF_INET6} +IPVERSION_BUFFER = {4: ADDR_BUFFER_4, 6: ADDR_BUFFER_6} + +ENTRY_IS_LOWER = -1 +ENTRY_MATCHES = 0 +ENTRY_IS_HIGHER = 1 diff --git a/neutron/privileged/agent/linux/netlink_lib.py b/neutron/privileged/agent/linux/netlink_lib.py new file mode 100644 index 00000000000..307930b7ae8 --- /dev/null +++ b/neutron/privileged/agent/linux/netlink_lib.py @@ -0,0 +1,275 @@ +# 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. +# +# Some parts are based on python-conntrack: +# Copyright (c) 2009-2011,2015 Andrew Grigorev +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +import ctypes +from ctypes import util +import re + +from neutron_lib import constants +from oslo_log import log as logging + +from neutron._i18n import _, _LW +from neutron.common import exceptions +from neutron import privileged +from neutron.privileged.agent.linux import netlink_constants as nl_constants + +LOG = logging.getLogger(__name__) + +nfct = ctypes.CDLL(util.find_library('netfilter_conntrack')) +libc = ctypes.CDLL(util.find_library('libc.so.6')) + +IP_VERSIONS = [constants.IP_VERSION_4, constants.IP_VERSION_6] +DATA_CALLBACK = None + +# position of attribute in raw conntrack entry +ATTR_POSITIONS = { + 'icmp': [('type', 6), ('code', 7), ('src', 4), ('dst', 5), ('id', 8), + ('zone', 16)], + 'icmpv6': [('type', 6), ('code', 7), ('src', 4), ('dst', 5), ('id', 8), + ('zone', 16)], + 'tcp': [('sport', 7), ('dport', 8), ('src', 5), ('dst', 6), ('zone', 15)], + 'udp': [('sport', 6), ('dport', 7), ('src', 4), ('dst', 5), ('zone', 14)] +} + +TARGET = {'src': {4: nl_constants.ATTR_IPV4_SRC, + 6: nl_constants.ATTR_IPV6_SRC}, + 'dst': {4: nl_constants.ATTR_IPV4_DST, + 6: nl_constants.ATTR_IPV6_DST}, + 'ipversion': {4: nl_constants.ATTR_L3PROTO, + 6: nl_constants.ATTR_L3PROTO}, + 'protocol': {4: nl_constants.ATTR_L4PROTO, + 6: nl_constants.ATTR_L4PROTO}, + 'code': {4: nl_constants.ATTR_ICMP_CODE, + 6: nl_constants.ATTR_ICMP_CODE}, + 'type': {4: nl_constants.ATTR_ICMP_TYPE, + 6: nl_constants.ATTR_ICMP_TYPE}, + 'id': {4: nl_constants.ATTR_ICMP_ID, + 6: nl_constants.ATTR_ICMP_ID}, + 'sport': {4: nl_constants.ATTR_PORT_SRC, + 6: nl_constants.ATTR_PORT_SRC}, + 'dport': {4: nl_constants.ATTR_PORT_DST, + 6: nl_constants.ATTR_PORT_DST}, + 'zone': {4: nl_constants.ATTR_ZONE, + 6: nl_constants.ATTR_ZONE} + } + +NFCT_CALLBACK = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, + ctypes.c_void_p, ctypes.c_void_p) + + +class ConntrackManager(object): + def __init__(self, family_socket=None): + self.family_socket = family_socket + self.set_functions = { + 'src': {4: nfct.nfct_set_attr, + 6: nfct.nfct_set_attr}, + 'dst': {4: nfct.nfct_set_attr, + 6: nfct.nfct_set_attr}, + 'ipversion': {4: nfct.nfct_set_attr_u8, + 6: nfct.nfct_set_attr_u8}, + 'protocol': {4: nfct.nfct_set_attr_u8, + 6: nfct.nfct_set_attr_u8}, + 'type': {4: nfct.nfct_set_attr_u8, + 6: nfct.nfct_set_attr_u8}, + 'code': {4: nfct.nfct_set_attr_u8, + 6: nfct.nfct_set_attr_u8}, + 'id': {4: nfct.nfct_set_attr_u16, + 6: nfct.nfct_set_attr_u16}, + 'sport': {4: nfct.nfct_set_attr_u16, + 6: nfct.nfct_set_attr_u16}, + 'dport': {4: nfct.nfct_set_attr_u16, + 6: nfct.nfct_set_attr_u16}, + 'zone': {4: nfct.nfct_set_attr_u16, + 6: nfct.nfct_set_attr_u16} + } + + self.converters = {'src': str, + 'dst': str, + 'ipversion': nl_constants.IPVERSION_SOCKET.get, + 'protocol': constants.IP_PROTOCOL_MAP.get, + 'code': int, + 'type': int, + 'id': libc.htons, + 'sport': libc.htons, + 'dport': libc.htons, + 'zone': int + } + + def list_entries(self): + entries = [] + raw_entry = ctypes.create_string_buffer(nl_constants.BUFFER) + + @NFCT_CALLBACK + def callback(type_, conntrack, data): + nfct.nfct_snprintf(raw_entry, nl_constants.BUFFER, + conntrack, type_, + nl_constants.NFCT_O_PLAIN, + nl_constants.NFCT_OF_TIME) + entries.append(raw_entry.value) + return nl_constants.NFCT_CB_CONTINUE + + self._callback_register(nl_constants.NFCT_T_ALL, + callback, DATA_CALLBACK) + + data_ref = self._get_ref(self.family_socket or + nl_constants.IPVERSION_SOCKET[4]) + self._query(nl_constants.NFCT_Q_DUMP, data_ref) + return entries + + def delete_entries(self, entries): + conntrack = nfct.nfct_new() + try: + for entry in entries: + self._set_attributes(conntrack, entry) + self._query(nl_constants.NFCT_Q_DESTROY, conntrack) + except Exception as e: + msg = _("Failed to delete conntrack entries %s") % e + LOG.critical(msg) + raise exceptions.CTZoneExhaustedError() + finally: + nfct.nfct_destroy(conntrack) + + def _query(self, query_type, query_data): + result = nfct.nfct_query(self.conntrack_handler, query_type, + query_data) + if result == nl_constants.NFCT_CB_FAILURE: + LOG.warning(_LW("Netlink query failed")) + + def _convert_text_to_binary(self, source, addr_family): + dest = ctypes.create_string_buffer( + nl_constants.IPVERSION_BUFFER[addr_family]) + libc.inet_pton(nl_constants.IPVERSION_SOCKET[addr_family], + source, dest) + return dest.raw + + def _set_attributes(self, conntrack, entry): + ipversion = entry.get('ipversion', 4) + for attr, value in entry.items(): + set_function = self.set_functions[attr][ipversion] + target = TARGET[attr][ipversion] + converter = self.converters[attr] + if attr in ['src', 'dst']: + # convert src and dst of IPv4 and IPv6 into same format + value = self._convert_text_to_binary(value, ipversion) + set_function(conntrack, target, converter(value)) + + def _callback_register(self, message_type, callback_func, data): + nfct.nfct_callback_register(self.conntrack_handler, + message_type, callback_func, data) + + def _get_ref(self, data): + return ctypes.byref(ctypes.c_int(data)) + + def __enter__(self): + self.conntrack_handler = nfct.nfct_open( + nl_constants.CONNTRACK, + nl_constants.NFNL_SUBSYS_CTNETLINK) + if not self.conntrack_handler: + msg = _("Failed to open new conntrack handler") + LOG.critical(msg) + raise exceptions.CTZoneExhaustedError() + return self + + def __exit__(self, *args): + nfct.nfct_close(self.conntrack_handler) + + +def _parse_entry(entry, ipversion, zone): + """Parse entry from text to Python tuple + + :param entry: raw conntrack entry + :param ipversion: ip version 4 or 6 + :return: conntrack entry in Python tuple in format + (ipversion, protocol, sport, dport, src_ip, dst_ip, zone) + example: (4, 'tcp', '1', '2', '1.1.1.1', '2.2.2.2', 1) + The attributes are ordered to be easy to compare with other entries + and compare with firewall rule + """ + protocol = entry[1] + parsed_entry = [ipversion, protocol] + for attr, position in ATTR_POSITIONS[protocol]: + val = entry[position].partition('=')[2] + try: + parsed_entry.append(int(val)) + except ValueError: + parsed_entry.append(val) + parsed_entry[-1] = zone + return tuple(parsed_entry) + + +@privileged.default.entrypoint +def list_entries(zone): + """List and parse all conntrack entries in zone + + :param zone: zone in which entries belong to + :return: sorted list of conntrack entries in Python tuple with sort key + is dest port + example: [(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')] + """ + parsed_entries = [] + for ipversion in IP_VERSIONS: + with ConntrackManager(nl_constants.IPVERSION_SOCKET[ipversion]) \ + as conntrack: + raw_entries = [entry for entry in conntrack.list_entries() if + re.search(r'\bzone={}\b'.format(zone), entry) is + not None] + + for raw_entry in raw_entries: + _entry = raw_entry.split() + parsed_entry = _parse_entry(_entry, ipversion, zone) + parsed_entries.append(parsed_entry) + # sort by dest port + return sorted(parsed_entries, key=lambda x: x[3]) + + +@privileged.default.entrypoint +def delete_entries(entries): + """Delete selected entries + + :param entries: list of parsed (as tuple) entries to delete + :return: None + """ + entry_args = [] + for entry in entries: + entry_arg = {'ipversion': entry[0], 'protocol': entry[1]} + for idx, attr in enumerate(ATTR_POSITIONS[entry_arg['protocol']]): + entry_arg[attr[0]] = entry[idx + 2] + entry_args.append(entry_arg) + + with ConntrackManager() as conntrack: + conntrack.delete_entries(entry_args) diff --git a/neutron/tests/functional/agent/linux/test_netlink_lib.py b/neutron/tests/functional/agent/linux/test_netlink_lib.py new file mode 100644 index 00000000000..2468c33f9bc --- /dev/null +++ b/neutron/tests/functional/agent/linux/test_netlink_lib.py @@ -0,0 +1,110 @@ +# 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 neutron.agent.linux import utils as linux_utils +from neutron.privileged.agent.linux import netlink_lib as nl_lib +from neutron.tests.functional import base as functional_base + + +class NetlinkLibTestCase(functional_base.BaseSudoTestCase): + """Functional test for netlink_lib: List, delete, flush conntrack entries. + + For each function, first we add a specific namespace, then create real + conntrack entries. netlink_lib function will do list, delete and flush + these entries. This class will test this netlink_lib function work + as expected. + """ + + def _create_entries(self, zone): + conntrack_cmds = ( + ['conntrack', '-I', '-p', 'tcp', + '-s', '1.1.1.1', '-d', '2.2.2.2', + '--sport', '1', '--dport', '2', + '--state', 'ESTABLISHED', '--timeout', '1234', '-w', zone], + ['conntrack', '-I', '-p', 'udp', + '-s', '1.1.1.1', '-d', '2.2.2.2', + '--sport', '4', '--dport', '5', + '--timeout', '1234', '-w', zone], + ['conntrack', '-I', '-p', 'icmp', + '-s', '1.1.1.1', '-d', '2.2.2.2', + '--icmp-type', '8', '--icmp-code', '0', '--icmp-id', '3333', + '--timeout', '1234', '-w', zone], + ) + + for cmd in conntrack_cmds: + try: + linux_utils.execute(cmd, + run_as_root=True, + check_exit_code=True, + extra_ok_codes=[1]) + except RuntimeError: + raise Exception('Error while creating entry') + + def _delete_entry(self, delete_entries, remain_entries, zone): + nl_lib.delete_entries(entries=delete_entries) + entries_list = nl_lib.list_entries(zone=zone) + self.assertEqual(remain_entries, entries_list) + + def test_list_entries(self): + _zone = 10 + self._create_entries(zone=_zone) + expected = ( + (4, 'icmp', 8, 0, '1.1.1.1', '2.2.2.2', 3333, _zone), + (4, 'tcp', 1, 2, '1.1.1.1', '2.2.2.2', _zone), + (4, 'udp', 4, 5, '1.1.1.1', '2.2.2.2', _zone) + ) + entries_list = nl_lib.list_entries(zone=_zone) + self.assertEqual(expected, entries_list) + + def test_delete_icmp_entry(self): + _zone = 20 + self._create_entries(zone=_zone) + icmp_entry = [(4, 'icmp', 8, 0, '1.1.1.1', '2.2.2.2', 3333, _zone)] + remain_entries = ( + (4, 'tcp', 1, 2, '1.1.1.1', '2.2.2.2', _zone), + (4, 'udp', 4, 5, '1.1.1.1', '2.2.2.2', _zone), + ) + self._delete_entry(icmp_entry, remain_entries, _zone) + + def test_delete_tcp_entry(self): + _zone = 30 + self._create_entries(zone=_zone) + tcp_entry = [(4, 'tcp', 1, 2, '1.1.1.1', '2.2.2.2', _zone)] + remain_entries = ( + (4, 'icmp', 8, 0, '1.1.1.1', '2.2.2.2', 3333, _zone), + (4, 'udp', 4, 5, '1.1.1.1', '2.2.2.2', _zone) + ) + self._delete_entry(tcp_entry, remain_entries, _zone) + + def test_delete_udp_entry(self): + _zone = 40 + self._create_entries(zone=_zone) + udp_entry = [(4, 'udp', 4, 5, '1.1.1.1', '2.2.2.2', _zone)] + remain_entries = ( + (4, 'icmp', 8, 0, '1.1.1.1', '2.2.2.2', 3333, _zone), + (4, 'tcp', 1, 2, '1.1.1.1', '2.2.2.2', _zone) + ) + self._delete_entry(udp_entry, remain_entries, _zone) + + def test_delete_multiple_entries(self): + _zone = 50 + self._create_entries(zone=_zone) + delete_entries = ( + (4, 'icmp', 8, 0, '1.1.1.1', '2.2.2.2', 3333, _zone), + (4, 'tcp', 1, 2, '1.1.1.1', '2.2.2.2', _zone), + (4, 'udp', 4, 5, '1.1.1.1', '2.2.2.2', _zone) + ) + remain_entries = () + self._delete_entry(delete_entries, remain_entries, _zone) diff --git a/neutron/tests/unit/privileged/__init__.py b/neutron/tests/unit/privileged/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/tests/unit/privileged/agent/__init__.py b/neutron/tests/unit/privileged/agent/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/tests/unit/privileged/agent/linux/__init__.py b/neutron/tests/unit/privileged/agent/linux/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/tests/unit/privileged/agent/linux/test_netlink_lib.py b/neutron/tests/unit/privileged/agent/linux/test_netlink_lib.py new file mode 100644 index 00000000000..00cf5c98577 --- /dev/null +++ b/neutron/tests/unit/privileged/agent/linux/test_netlink_lib.py @@ -0,0 +1,342 @@ +# 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_lib import constants +import testtools + +from neutron.common import exceptions +from neutron.privileged.agent.linux import netlink_constants as nl_constants +from neutron.privileged.agent.linux import netlink_lib as nl_lib +from neutron.tests import base + +FAKE_ICMP_ENTRY = {'ipversion': 4, 'protocol': 'icmp', + 'type': '8', 'code': '0', 'id': 1234, + 'src': '1.1.1.1', 'dst': '2.2.2.2', 'zone': 1} +FAKE_TCP_ENTRY = {'ipversion': 4, 'protocol': 'tcp', + 'sport': 1, 'dport': 2, + 'src': '1.1.1.1', 'dst': '2.2.2.2', 'zone': 1} +FAKE_UDP_ENTRY = {'ipversion': 4, 'protocol': 'udp', + 'sport': 1, 'dport': 2, + 'src': '1.1.1.1', 'dst': '2.2.2.2', 'zone': 1} + + +class NetlinkLibTestCase(base.BaseTestCase): + def setUp(self): + super(NetlinkLibTestCase, self).setUp() + nl_lib.nfct = mock.Mock() + nl_lib.libc = mock.Mock() + + def test_open_new_conntrack_handler_failed(self): + nl_lib.nfct.nfct_open.return_value = None + with testtools.ExpectedException(exceptions.CTZoneExhaustedError): + with nl_lib.ConntrackManager(): + nl_lib.nfct.nfct_open.assert_called_once_with() + nl_lib.nfct.nfct_close.assert_not_called() + + def test_open_new_conntrack_handler_pass(self): + with nl_lib.ConntrackManager(): + nl_lib.nfct.nfct_open.assert_called_once_with( + nl_constants.CONNTRACK, nl_constants.NFNL_SUBSYS_CTNETLINK) + nl_lib.nfct.nfct_close.assert_called_once_with(nl_lib.nfct.nfct_open( + nl_constants.CONNTRACK, nl_constants.NFNL_SUBSYS_CTNETLINK)) + + def test_conntrack_list_entries(self): + with nl_lib.ConntrackManager() as conntrack: + + nl_lib.nfct.nfct_open.assert_called_once_with( + nl_constants.CONNTRACK, nl_constants.NFNL_SUBSYS_CTNETLINK) + + conntrack.list_entries() + + nl_lib.nfct.nfct_callback_register.assert_has_calls( + [mock.call(nl_lib.nfct.nfct_open(), nl_constants.NFCT_T_ALL, + mock.ANY, None)]) + nl_lib.nfct.nfct_query.assert_called_once_with( + nl_lib.nfct.nfct_open( + nl_constants.CONNTRACK, + nl_constants.NFNL_SUBSYS_CTNETLINK), + nl_constants.NFCT_Q_DUMP, + mock.ANY) + nl_lib.nfct.nfct_close.assert_called_once_with(nl_lib.nfct.nfct_open( + nl_constants.CONNTRACK, nl_constants.NFNL_SUBSYS_CTNETLINK)) + + def test_conntrack_new_failed(self): + nl_lib.nfct.nfct_new.return_value = None + with nl_lib.ConntrackManager() as conntrack: + nl_lib.nfct.nfct_open.assert_called_once_with( + nl_constants.CONNTRACK, + nl_constants.NFNL_SUBSYS_CTNETLINK) + conntrack.delete_entries([FAKE_ICMP_ENTRY]) + nl_lib.nfct.nfct_new.assert_called_once_with() + nl_lib.nfct.nfct_destroy.assert_called_once_with(None) + nl_lib.nfct.nfct_close.assert_called_once_with(nl_lib.nfct.nfct_open( + nl_constants.CONNTRACK, + nl_constants.NFNL_SUBSYS_CTNETLINK)) + + def test_conntrack_delete_icmp_entry(self): + conntrack_filter = mock.Mock() + nl_lib.nfct.nfct_new.return_value = conntrack_filter + with nl_lib.ConntrackManager() as conntrack: + nl_lib.nfct.nfct_open.assert_called_once_with( + nl_constants.CONNTRACK, + nl_constants.NFNL_SUBSYS_CTNETLINK) + conntrack.delete_entries([FAKE_ICMP_ENTRY]) + calls = [ + mock.call(conntrack_filter, + nl_constants.ATTR_L3PROTO, + nl_constants.IPVERSION_SOCKET[4]), + mock.call(conntrack_filter, + nl_constants.ATTR_L4PROTO, + constants.IP_PROTOCOL_MAP['icmp']), + mock.call(conntrack_filter, + nl_constants.ATTR_ICMP_CODE, + int(FAKE_ICMP_ENTRY['code'])), + mock.call(conntrack_filter, + nl_constants.ATTR_ICMP_TYPE, + int(FAKE_ICMP_ENTRY['type'])) + ] + nl_lib.nfct.nfct_set_attr_u8.assert_has_calls(calls, + any_order=True) + calls = [ + mock.call(conntrack_filter, + nl_constants.ATTR_ICMP_ID, + nl_lib.libc.htons(FAKE_ICMP_ENTRY['id'])), + mock.call(conntrack_filter, + nl_constants.ATTR_ZONE, + int(FAKE_ICMP_ENTRY['zone'])) + ] + nl_lib.nfct.nfct_set_attr_u16.assert_has_calls(calls, + any_order=True) + calls = [ + mock.call(conntrack_filter, + nl_constants.ATTR_IPV4_SRC, + str(conntrack._convert_text_to_binary( + FAKE_ICMP_ENTRY['src'], 4)) + ), + mock.call(conntrack_filter, + nl_constants.ATTR_IPV4_DST, + str(conntrack._convert_text_to_binary( + FAKE_ICMP_ENTRY['dst'], 4)) + ), + ] + nl_lib.nfct.nfct_set_attr.assert_has_calls(calls, any_order=True) + nl_lib.nfct.nfct_destroy.assert_called_once_with(conntrack_filter) + nl_lib.nfct.nfct_close.assert_called_once_with(nl_lib.nfct.nfct_open( + nl_constants.CONNTRACK, + nl_constants.NFNL_SUBSYS_CTNETLINK)) + + def test_conntrack_delete_udp_entry(self): + conntrack_filter = mock.Mock() + nl_lib.nfct.nfct_new.return_value = conntrack_filter + with nl_lib.ConntrackManager() as conntrack: + nl_lib.nfct.nfct_open.assert_called_once_with( + nl_constants.CONNTRACK, + nl_constants.NFNL_SUBSYS_CTNETLINK) + conntrack.delete_entries([FAKE_UDP_ENTRY]) + calls = [ + mock.call(conntrack_filter, + nl_constants.ATTR_L3PROTO, + nl_constants.IPVERSION_SOCKET[4]), + mock.call(conntrack_filter, + nl_constants.ATTR_L4PROTO, + constants.IP_PROTOCOL_MAP['udp']) + ] + nl_lib.nfct.nfct_set_attr_u8.assert_has_calls(calls, + any_order=True) + calls = [ + mock.call(conntrack_filter, + nl_constants.ATTR_PORT_SRC, + nl_lib.libc.htons(FAKE_UDP_ENTRY['sport'])), + mock.call(conntrack_filter, + nl_constants.ATTR_PORT_DST, + nl_lib.libc.htons(FAKE_UDP_ENTRY['dport'])), + mock.call(conntrack_filter, + nl_constants.ATTR_ZONE, + int(FAKE_ICMP_ENTRY['zone'])) + ] + nl_lib.nfct.nfct_set_attr_u16.assert_has_calls(calls, + any_order=True) + calls = [ + mock.call(conntrack_filter, + nl_constants.ATTR_IPV4_SRC, + str(conntrack._convert_text_to_binary( + FAKE_UDP_ENTRY['src'], 4)) + ), + mock.call(conntrack_filter, + nl_constants.ATTR_IPV4_DST, + str(conntrack._convert_text_to_binary( + FAKE_UDP_ENTRY['dst'], 4)) + ), + ] + nl_lib.nfct.nfct_set_attr.assert_has_calls(calls, any_order=True) + nl_lib.nfct.nfct_destroy.assert_called_once_with(conntrack_filter) + nl_lib.nfct.nfct_close.assert_called_once_with(nl_lib.nfct.nfct_open( + nl_constants.CONNTRACK, + nl_constants.NFNL_SUBSYS_CTNETLINK)) + + def test_conntrack_delete_tcp_entry(self): + conntrack_filter = mock.Mock() + nl_lib.nfct.nfct_new.return_value = conntrack_filter + with nl_lib.ConntrackManager() as conntrack: + nl_lib.nfct.nfct_open.assert_called_once_with( + nl_constants.CONNTRACK, + nl_constants.NFNL_SUBSYS_CTNETLINK) + conntrack.delete_entries([FAKE_TCP_ENTRY]) + calls = [ + mock.call(conntrack_filter, + nl_constants.ATTR_L3PROTO, + nl_constants.IPVERSION_SOCKET[4]), + mock.call(conntrack_filter, + nl_constants.ATTR_L4PROTO, + constants.IP_PROTOCOL_MAP['tcp']) + ] + nl_lib.nfct.nfct_set_attr_u8.assert_has_calls(calls, + any_order=True) + calls = [ + mock.call(conntrack_filter, + nl_constants.ATTR_PORT_SRC, + nl_lib.libc.htons(FAKE_TCP_ENTRY['sport'])), + mock.call(conntrack_filter, + nl_constants.ATTR_PORT_DST, + nl_lib.libc.htons(FAKE_TCP_ENTRY['dport'])), + mock.call(conntrack_filter, + nl_constants.ATTR_ZONE, + int(FAKE_ICMP_ENTRY['zone'])) + ] + nl_lib.nfct.nfct_set_attr_u16.assert_has_calls(calls, + any_order=True) + calls = [ + mock.call(conntrack_filter, + nl_constants.ATTR_IPV4_SRC, + str(conntrack._convert_text_to_binary( + FAKE_TCP_ENTRY['src'], 4)) + ), + mock.call(conntrack_filter, + nl_constants.ATTR_IPV4_DST, + str(conntrack._convert_text_to_binary( + FAKE_TCP_ENTRY['dst'], 4)) + ), + ] + + nl_lib.nfct.nfct_set_attr.assert_has_calls(calls, any_order=True) + nl_lib.nfct.nfct_destroy.assert_called_once_with(conntrack_filter) + nl_lib.nfct.nfct_close.assert_called_once_with(nl_lib.nfct.nfct_open( + nl_constants.CONNTRACK, + nl_constants.NFNL_SUBSYS_CTNETLINK)) + + def test_conntrack_delete_entries(self): + conntrack_filter = mock.Mock() + nl_lib.nfct.nfct_new.return_value = conntrack_filter + with nl_lib.ConntrackManager() as conntrack: + nl_lib.nfct.nfct_open.assert_called_once_with( + nl_constants.CONNTRACK, + nl_constants.NFNL_SUBSYS_CTNETLINK) + conntrack.delete_entries([FAKE_ICMP_ENTRY, + FAKE_TCP_ENTRY, + FAKE_UDP_ENTRY]) + calls = [ + mock.call(conntrack_filter, + nl_constants.ATTR_L3PROTO, + nl_constants.IPVERSION_SOCKET[4]), + mock.call(conntrack_filter, + nl_constants.ATTR_L4PROTO, + constants.IP_PROTOCOL_MAP['tcp']), + mock.call(conntrack_filter, + nl_constants.ATTR_L3PROTO, + nl_constants.IPVERSION_SOCKET[4]), + mock.call(conntrack_filter, + nl_constants.ATTR_L4PROTO, + constants.IP_PROTOCOL_MAP['udp']), + mock.call(conntrack_filter, + nl_constants.ATTR_L3PROTO, + nl_constants.IPVERSION_SOCKET[4]), + mock.call(conntrack_filter, + nl_constants.ATTR_L4PROTO, + constants.IP_PROTOCOL_MAP['icmp']), + mock.call(conntrack_filter, + nl_constants.ATTR_ICMP_CODE, + int(FAKE_ICMP_ENTRY['code'])), + mock.call(conntrack_filter, + nl_constants.ATTR_ICMP_TYPE, + int(FAKE_ICMP_ENTRY['type'])) + ] + nl_lib.nfct.nfct_set_attr_u8.assert_has_calls(calls, + any_order=True) + calls = [ + mock.call(conntrack_filter, + nl_constants.ATTR_PORT_SRC, + nl_lib.libc.htons(FAKE_TCP_ENTRY['sport'])), + mock.call(conntrack_filter, + nl_constants.ATTR_PORT_DST, + nl_lib.libc.htons(FAKE_TCP_ENTRY['dport'])), + mock.call(conntrack_filter, + nl_constants.ATTR_ZONE, + int(FAKE_TCP_ENTRY['zone'])), + mock.call(conntrack_filter, + nl_constants.ATTR_PORT_SRC, + nl_lib.libc.htons(FAKE_UDP_ENTRY['sport'])), + mock.call(conntrack_filter, + nl_constants.ATTR_PORT_DST, + nl_lib.libc.htons(FAKE_UDP_ENTRY['dport'])), + mock.call(conntrack_filter, + nl_constants.ATTR_ZONE, + int(FAKE_UDP_ENTRY['zone'])), + mock.call(conntrack_filter, + nl_constants.ATTR_ICMP_ID, + nl_lib.libc.htons(FAKE_ICMP_ENTRY['id'])), + mock.call(conntrack_filter, + nl_constants.ATTR_ZONE, + int(FAKE_ICMP_ENTRY['zone'])) + ] + nl_lib.nfct.nfct_set_attr_u16.assert_has_calls(calls, + any_order=True) + calls = [ + mock.call(conntrack_filter, + nl_constants.ATTR_IPV4_SRC, + str(conntrack._convert_text_to_binary( + FAKE_TCP_ENTRY['src'], 4)) + ), + mock.call(conntrack_filter, + nl_constants.ATTR_IPV4_DST, + str(conntrack._convert_text_to_binary( + FAKE_TCP_ENTRY['dst'], 4))), + mock.call(conntrack_filter, + nl_constants.ATTR_IPV4_SRC, + str(conntrack._convert_text_to_binary( + FAKE_UDP_ENTRY['src'], 4)) + ), + mock.call(conntrack_filter, + nl_constants.ATTR_IPV4_DST, + str(conntrack._convert_text_to_binary( + FAKE_UDP_ENTRY['dst'], 4)) + ), + mock.call(conntrack_filter, + nl_constants.ATTR_IPV4_SRC, + str(conntrack._convert_text_to_binary( + FAKE_ICMP_ENTRY['src'], 4)) + ), + mock.call(conntrack_filter, + nl_constants.ATTR_IPV4_DST, + str(conntrack._convert_text_to_binary( + FAKE_ICMP_ENTRY['dst'], 4)) + ), + ] + nl_lib.nfct.nfct_set_attr.assert_has_calls(calls, any_order=True) + nl_lib.nfct.nfct_destroy.assert_called_once_with(conntrack_filter) + nl_lib.nfct.nfct_close.assert_called_once_with(nl_lib.nfct.nfct_open( + nl_constants.CONNTRACK, + nl_constants.NFNL_SUBSYS_CTNETLINK))