From: shihanzhang Date: Mon, 4 Aug 2014 09:31:01 +0000 (+0800) Subject: Adds ipset support for Security Groups X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=2562a9271c828e982a74593e8fd07be13b0cfc4a;p=openstack-build%2Fneutron-build.git Adds ipset support for Security Groups Iptables chain is linear storage and filtering, when iptables rules are large, the load of l2 agent is heavy, this patch introduces ipset to security group for improving the security group performance. Change-Id: I6ff0ac519d0b9034d3bb5270885ed3cc1805674d Implements: blueprint add-ipset-to-security DocImpact --- diff --git a/etc/neutron/plugins/ml2/ml2_conf.ini b/etc/neutron/plugins/ml2/ml2_conf.ini index 6325e714e..4fb1a4a36 100644 --- a/etc/neutron/plugins/ml2/ml2_conf.ini +++ b/etc/neutron/plugins/ml2/ml2_conf.ini @@ -65,3 +65,7 @@ # Controls if neutron security group is enabled or not. # It should be false when you use nova security group. # enable_security_group = True + +# Use ipset to speed-up the iptables security groups. Enabling ipset support +# requires that ipset is installed on L2 agent node. +# enable_ipset = True diff --git a/etc/neutron/rootwrap.d/ipset-firewall.filters b/etc/neutron/rootwrap.d/ipset-firewall.filters new file mode 100644 index 000000000..52c66373b --- /dev/null +++ b/etc/neutron/rootwrap.d/ipset-firewall.filters @@ -0,0 +1,12 @@ +# neutron-rootwrap command filters for nodes on which neutron is +# expected to control network +# +# This file should be owned by (and only-writeable by) the root user + +# format seems to be +# cmd-name: filter-name, raw-command, user, args + +[Filters] +# neutron/agent/linux/iptables_firewall.py +# "ipset", "-A", ... +ipset: CommandFilter, ipset, root diff --git a/neutron/agent/linux/ipset_manager.py b/neutron/agent/linux/ipset_manager.py new file mode 100644 index 000000000..8728567f9 --- /dev/null +++ b/neutron/agent/linux/ipset_manager.py @@ -0,0 +1,75 @@ +# +# 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.common import utils + + +class IpsetManager(object): + """Wrapper for ipset.""" + + def __init__(self, execute=None, root_helper=None): + self.execute = execute or linux_utils.execute + self.root_helper = root_helper + + @utils.synchronized('ipset', external=True) + def create_ipset_chain(self, chain_name, ethertype): + cmd = ['ipset', 'create', '-exist', chain_name, 'hash:ip', 'family', + self._get_ipset_chain_type(ethertype)] + self._apply(cmd) + + @utils.synchronized('ipset', external=True) + def add_member_to_ipset_chain(self, chain_name, member_ip): + cmd = ['ipset', 'add', '-exist', chain_name, member_ip] + self._apply(cmd) + + @utils.synchronized('ipset', external=True) + def refresh_ipset_chain_by_name(self, chain_name, member_ips, ethertype): + new_chain_name = chain_name + '-new' + chain_type = self._get_ipset_chain_type(ethertype) + process_input = ["create %s hash:ip family %s" % (new_chain_name, + chain_type)] + for ip in member_ips: + process_input.append("add %s %s" % (new_chain_name, ip)) + + self._restore_ipset_chains(process_input) + self._swap_ipset_chains(new_chain_name, chain_name) + self._destroy_ipset_chain(new_chain_name) + + @utils.synchronized('ipset', external=True) + def del_ipset_chain_member(self, chain_name, member_ip): + cmd = ['ipset', 'del', chain_name, member_ip] + self._apply(cmd) + + @utils.synchronized('ipset', external=True) + def destroy_ipset_chain_by_name(self, chain_name): + self._destroy_ipset_chain(chain_name) + + def _apply(self, cmd, input=None): + input = '\n'.join(input) if input else None + self.execute(cmd, root_helper=self.root_helper, process_input=input) + + def _get_ipset_chain_type(self, ethertype): + return 'inet6' if ethertype == 'IPv6' else 'inet' + + def _restore_ipset_chains(self, process_input): + cmd = ['ipset', 'restore', '-exist'] + self._apply(cmd, process_input) + + def _swap_ipset_chains(self, src_chain, dest_chain): + cmd = ['ipset', 'swap', src_chain, dest_chain] + self._apply(cmd) + + def _destroy_ipset_chain(self, chain_name): + cmd = ['ipset', 'destroy', chain_name] + self._apply(cmd) diff --git a/neutron/agent/linux/iptables_firewall.py b/neutron/agent/linux/iptables_firewall.py index ec37306a5..f38800b55 100644 --- a/neutron/agent/linux/iptables_firewall.py +++ b/neutron/agent/linux/iptables_firewall.py @@ -17,6 +17,7 @@ import netaddr from oslo.config import cfg from neutron.agent import firewall +from neutron.agent.linux import ipset_manager from neutron.agent.linux import iptables_manager from neutron.common import constants from neutron.common import ipv6_utils @@ -33,7 +34,12 @@ CHAIN_NAME_PREFIX = {INGRESS_DIRECTION: 'i', SPOOF_FILTER: 's'} DIRECTION_IP_PREFIX = {'ingress': 'source_ip_prefix', 'egress': 'dest_ip_prefix'} +IPSET_DIRECTION = {INGRESS_DIRECTION: 'src', + EGRESS_DIRECTION: 'dst'} LINUX_DEV_LEN = 14 +IPSET_CHAIN_LEN = 20 +IPSET_CHANGE_BULK_THRESHOLD = 10 +IPSET_ADD_BULK_THRESHOLD = 5 class IptablesFirewallDriver(firewall.FirewallDriver): @@ -42,9 +48,13 @@ class IptablesFirewallDriver(firewall.FirewallDriver): EGRESS_DIRECTION: 'physdev-in'} def __init__(self): + self.root_helper = cfg.CONF.AGENT.root_helper self.iptables = iptables_manager.IptablesManager( - root_helper=cfg.CONF.AGENT.root_helper, + root_helper=self.root_helper, use_ipv6=ipv6_utils.is_enabled()) + # TODO(majopela, shihanzhang): refactor out ipset to a separate + # driver composed over this one + self.ipset = ipset_manager.IpsetManager(root_helper=self.root_helper) # list of port which has security group self.filtered_ports = {} self._add_fallback_chain_v4v6() @@ -56,6 +66,8 @@ class IptablesFirewallDriver(firewall.FirewallDriver): # List of security group member ips for ports residing on this host self.sg_members = {} self.pre_sg_members = None + self.ipset_chains = {} + self.enable_ipset = cfg.CONF.SECURITYGROUP.enable_ipset @property def ports(self): @@ -273,6 +285,9 @@ class IptablesFirewallDriver(firewall.FirewallDriver): for sg_id in sg_ids: for rule in self.sg_rules.get(sg_id, []): if rule['direction'] == direction: + if self.enable_ipset: + port_rules.append(rule) + continue remote_group_id = rule.get('remote_group_id') if not remote_group_id: port_rules.append(rule) @@ -288,11 +303,25 @@ class IptablesFirewallDriver(firewall.FirewallDriver): port_rules.append(ip_rule) return port_rules + def _get_remote_sg_ids(self, port, direction): + sg_ids = port.get('security_groups', []) + remote_sg_ids = [] + for sg_id in sg_ids: + remote_sg_ids.extend([rule['remote_group_id'] + for rule in self.sg_rules.get(sg_id, []) if + rule['direction'] == direction + and rule.get('remote_group_id')]) + return remote_sg_ids + def _add_rule_by_security_group(self, port, direction): chain_name = self._port_chain_name(port, direction) # select rules for current direction security_group_rules = self._select_sgr_by_direction(port, direction) security_group_rules += self._select_sg_rules_for_port(port, direction) + if self.enable_ipset: + remote_sg_ids = self._get_remote_sg_ids(port, direction) + # update the corresponding ipset chain member + self._update_ipset_chain_member(remote_sg_ids) # split groups by ip version # for ipv4, iptables command is used # for ipv6, iptables6 command is used @@ -315,11 +344,96 @@ class IptablesFirewallDriver(firewall.FirewallDriver): ipv4_iptables_rule, ipv6_iptables_rule) + def _get_cur_sg_member_ips(self, sg_id, ethertype): + return self.sg_members.get(sg_id, {}).get(ethertype, []) + + def _get_pre_sg_member_ips(self, sg_id, ethertype): + return self.pre_sg_members.get(sg_id, {}).get(ethertype, []) + + def _get_new_sg_member_ips(self, sg_id, ethertype): + add_member_ips = (set(self._get_cur_sg_member_ips(sg_id, ethertype)) - + set(self._get_pre_sg_member_ips(sg_id, ethertype))) + return list(add_member_ips) + + def _get_deleted_sg_member_ips(self, sg_id, ethertype): + del_member_ips = (set(self._get_pre_sg_member_ips(sg_id, ethertype)) - + set(self._get_cur_sg_member_ips(sg_id, ethertype))) + return list(del_member_ips) + + def _bulk_set_ips_to_chain(self, chain_name, member_ips, ethertype): + self.ipset.refresh_ipset_chain_by_name(chain_name, member_ips, + ethertype) + self.ipset_chains[chain_name] = member_ips + + def _add_ips_to_ipset_chain(self, chain_name, add_ips): + for ip in add_ips: + if ip not in self.ipset_chains[chain_name]: + self.ipset.add_member_to_ipset_chain(chain_name, ip) + self.ipset_chains[chain_name].append(ip) + + def _del_ips_from_ipset_chain(self, chain_name, del_ips): + if chain_name in self.ipset_chains: + for del_ip in del_ips: + if del_ip in self.ipset_chains[chain_name]: + self.ipset.del_ipset_chain_member(chain_name, del_ip) + self.ipset_chains[chain_name].remove(del_ip) + + def _update_ipset_chain_member(self, security_group_ids): + for sg_id in security_group_ids or []: + for ethertype in ['IPv4', 'IPv6']: + add_ips = self._get_new_sg_member_ips(sg_id, ethertype) + del_ips = self._get_deleted_sg_member_ips(sg_id, ethertype) + cur_member_ips = self._get_cur_sg_member_ips(sg_id, ethertype) + chain_name = ethertype + sg_id[:IPSET_CHAIN_LEN] + if chain_name not in self.ipset_chains: + self.ipset_chains[chain_name] = [] + self.ipset.create_ipset_chain( + chain_name, ethertype) + self._bulk_set_ips_to_chain(chain_name, + cur_member_ips, ethertype) + elif (len(add_ips) + len(del_ips) + < IPSET_CHANGE_BULK_THRESHOLD): + self._add_ips_to_ipset_chain(chain_name, add_ips) + self._del_ips_from_ipset_chain(chain_name, del_ips) + else: + self._bulk_set_ips_to_chain(chain_name, + cur_member_ips, ethertype) + + def _generate_ipset_chain(self, sg_rule, remote_gid): + iptables_rules = [] + args = self._protocol_arg(sg_rule.get('protocol')) + args += self._port_arg('sport', + sg_rule.get('protocol'), + sg_rule.get('source_port_range_min'), + sg_rule.get('source_port_range_max')) + args += self._port_arg('dport', + sg_rule.get('protocol'), + sg_rule.get('port_range_min'), + sg_rule.get('port_range_max')) + direction = sg_rule.get('direction') + ethertype = sg_rule.get('ethertype') + # the length of ipset chain name require less than 31 + # characters + ipset_chain_name = (ethertype + remote_gid[:IPSET_CHAIN_LEN]) + if ipset_chain_name in self.ipset_chains: + args += ['-m set', '--match-set', + ipset_chain_name, + IPSET_DIRECTION[direction]] + args += ['-j RETURN'] + iptables_rules += [' '.join(args)] + return iptables_rules + def _convert_sgr_to_iptables_rules(self, security_group_rules): iptables_rules = [] self._drop_invalid_packets(iptables_rules) self._allow_established(iptables_rules) for rule in security_group_rules: + if self.enable_ipset: + remote_gid = rule.get('remote_group_id') + if remote_gid: + iptables_rules.extend( + self._generate_ipset_chain(rule, remote_gid)) + continue # These arguments MUST be in the format iptables-save will # display them: source/dest, protocol, sport, dport, target # Otherwise the iptables_manager code won't be able to find @@ -422,6 +536,13 @@ class IptablesFirewallDriver(firewall.FirewallDriver): for remove_chain_id in need_removed_ipset_chains: if remove_chain_id in self.sg_members: self.sg_members.pop(remove_chain_id, None) + if self.enable_ipset: + for ethertype in ['IPv4', 'IPv6']: + removed_chain = ( + ethertype + remove_chain_id[:IPSET_CHAIN_LEN]) + if removed_chain in self.ipset_chains: + self.ipset.destroy_ipset_chain_by_name(removed_chain) + self.ipset_chains.pop(removed_chain, None) # Remove unused security group rules for remove_group_id in need_removed_security_groups: diff --git a/neutron/agent/securitygroups_rpc.py b/neutron/agent/securitygroups_rpc.py index 478e0f9e0..17b544502 100644 --- a/neutron/agent/securitygroups_rpc.py +++ b/neutron/agent/securitygroups_rpc.py @@ -37,7 +37,11 @@ security_group_opts = [ help=_( 'Controls whether the neutron security group API is enabled ' 'in the server. It should be false when using no security ' - 'groups or using the nova security group API.')) + 'groups or using the nova security group API.')), + cfg.BoolOpt( + 'enable_ipset', + default=True, + help=_('Use ipset to speed-up the iptables based security groups.')) ] cfg.CONF.register_opts(security_group_opts, 'SECURITYGROUP') diff --git a/neutron/tests/unit/test_iptables_firewall.py b/neutron/tests/unit/test_iptables_firewall.py index d1da6b275..f313df139 100644 --- a/neutron/tests/unit/test_iptables_firewall.py +++ b/neutron/tests/unit/test_iptables_firewall.py @@ -20,6 +20,7 @@ from oslo.config import cfg from neutron.agent.common import config as a_cfg from neutron.agent.linux import iptables_firewall +from neutron.agent import securitygroups_rpc as sg_cfg from neutron.common import constants from neutron.tests import base from neutron.tests.unit import test_api_v2 @@ -31,11 +32,16 @@ FAKE_PREFIX = {'IPv4': '10.0.0.0/24', FAKE_IP = {'IPv4': '10.0.0.1', 'IPv6': 'fe80::1'} +TEST_IP_RANGE = ['10.0.0.1', '10.0.0.2', '10.0.0.3', '10.0.0.4', + '10.0.0.5', '10.0.0.6', '10.0.0.7', '10.0.0.8', + '10.0.0.9', '10.0.0.10'] -class IptablesFirewallTestCase(base.BaseTestCase): + +class BaseIptablesFirewallTestCase(base.BaseTestCase): def setUp(self): - super(IptablesFirewallTestCase, self).setUp() + super(BaseIptablesFirewallTestCase, self).setUp() cfg.CONF.register_opts(a_cfg.ROOT_HELPER_OPTS, 'AGENT') + cfg.CONF.register_opts(sg_cfg.security_group_opts, 'SECURITYGROUP') self.utils_exec_p = mock.patch( 'neutron.agent.linux.utils.execute') self.utils_exec = self.utils_exec_p.start() @@ -52,6 +58,9 @@ class IptablesFirewallTestCase(base.BaseTestCase): self.firewall = iptables_firewall.IptablesFirewallDriver() self.firewall.iptables = self.iptables_inst + +class IptablesFirewallTestCase(BaseIptablesFirewallTestCase): + def _fake_port(self): return {'device': 'tapfake_dev', 'mac_address': 'ff:ff:ff:ff:ff:ff', @@ -1221,3 +1230,120 @@ class IptablesFirewallTestCase(base.BaseTestCase): mock.call.add_rule('ofake_dev', '-j $sg-fallback'), mock.call.add_rule('sg-chain', '-j ACCEPT')] self.v4filter_inst.assert_has_calls(calls) + + +class IptablesFirewallEnhancedIpsetTestCase(BaseIptablesFirewallTestCase): + def setUp(self): + super(IptablesFirewallEnhancedIpsetTestCase, self).setUp() + self.firewall.ipset = mock.Mock() + + def _fake_port(self): + return {'device': 'tapfake_dev', + 'mac_address': 'ff:ff:ff:ff:ff:ff', + 'fixed_ips': [FAKE_IP['IPv4'], + FAKE_IP['IPv6']], + 'security_groups': ['fake_sgid'], + 'security_group_source_groups': ['fake_sgid']} + + def _fake_sg_rule(self): + return {'fake_sgid': [ + {'direction': 'ingress', 'remote_group_id': 'fake_sgid'}]} + + def test_prepare_port_filter_with_default_sg(self): + self.firewall.sg_rules = self._fake_sg_rule() + self.firewall.sg_members = {'fake_sgid': { + 'IPv4': ['10.0.0.1', '10.0.0.2'], 'IPv6': ['fe80::1']}} + self.firewall.pre_sg_members = {} + port = self._fake_port() + self.firewall.prepare_port_filter(port) + calls = [mock.call.create_ipset_chain('IPv4fake_sgid', 'IPv4'), + mock.call.refresh_ipset_chain_by_name( + 'IPv4fake_sgid', ['10.0.0.1', '10.0.0.2'], 'IPv4'), + mock.call.create_ipset_chain('IPv6fake_sgid', 'IPv6'), + mock.call.refresh_ipset_chain_by_name( + 'IPv6fake_sgid', ['fe80::1'], 'IPv6')] + + self.firewall.ipset.assert_has_calls(calls) + + def test_prepare_port_filter_with_add_members_beyond_4(self): + self.firewall.sg_rules = self._fake_sg_rule() + self.firewall.sg_members = {'fake_sgid': { + 'IPv4': TEST_IP_RANGE[:5], + 'IPv6': ['fe80::1']}} + self.firewall.pre_sg_members = {} + port = self._fake_port() + self.firewall.prepare_port_filter(port) + calls = [mock.call.create_ipset_chain('IPv4fake_sgid', 'IPv4'), + mock.call.refresh_ipset_chain_by_name( + 'IPv4fake_sgid', TEST_IP_RANGE[:5], 'IPv4'), + mock.call.create_ipset_chain('IPv6fake_sgid', 'IPv6'), + mock.call.refresh_ipset_chain_by_name( + 'IPv6fake_sgid', ['fe80::1'], 'IPv6')] + + self.firewall.ipset.assert_has_calls(calls) + + def test_prepare_port_filter_with_ipset_chain_exist(self): + self.firewall.sg_rules = self._fake_sg_rule() + self.firewall.ipset_chains = {'IPv4fake_sgid': ['10.0.0.2']} + self.firewall.sg_members = {'fake_sgid': { + 'IPv4': TEST_IP_RANGE[:5], + 'IPv6': ['fe80::1']}} + self.firewall.pre_sg_members = {'fake_sgid': { + 'IPv4': ['10.0.0.2'], + 'IPv6': ['fe80::1']}} + port = self._fake_port() + self.firewall.prepare_port_filter(port) + calls = [ + mock.call.add_member_to_ipset_chain('IPv4fake_sgid', '10.0.0.1'), + mock.call.add_member_to_ipset_chain('IPv4fake_sgid', '10.0.0.3'), + mock.call.add_member_to_ipset_chain('IPv4fake_sgid', '10.0.0.4'), + mock.call.add_member_to_ipset_chain('IPv4fake_sgid', '10.0.0.5'), + mock.call.create_ipset_chain('IPv6fake_sgid', 'IPv6'), + mock.call.refresh_ipset_chain_by_name( + 'IPv6fake_sgid', ['fe80::1'], 'IPv6')] + + self.firewall.ipset.assert_has_calls(calls, True) + + def test_prepare_port_filter_with_del_member(self): + self.firewall.sg_rules = self._fake_sg_rule() + self.firewall.ipset_chains = {'IPv4fake_sgid': ['10.0.0.2']} + self.firewall.sg_members = {'fake_sgid': { + 'IPv4': [ + '10.0.0.1', '10.0.0.3', '10.0.0.4', '10.0.0.5'], + 'IPv6': ['fe80::1']}} + self.firewall.pre_sg_members = {'fake_sgid': { + 'IPv4': ['10.0.0.2'], + 'IPv6': ['fe80::1']}} + port = self._fake_port() + self.firewall.prepare_port_filter(port) + calls = [ + mock.call.add_member_to_ipset_chain('IPv4fake_sgid', '10.0.0.1'), + mock.call.add_member_to_ipset_chain('IPv4fake_sgid', '10.0.0.3'), + mock.call.add_member_to_ipset_chain('IPv4fake_sgid', '10.0.0.4'), + mock.call.add_member_to_ipset_chain('IPv4fake_sgid', '10.0.0.5'), + mock.call.del_ipset_chain_member('IPv4fake_sgid', '10.0.0.2'), + mock.call.create_ipset_chain('IPv6fake_sgid', 'IPv6'), + mock.call.refresh_ipset_chain_by_name( + 'IPv6fake_sgid', ['fe80::1'], 'IPv6')] + + self.firewall.ipset.assert_has_calls(calls, True) + + def test_prepare_port_filter_change_beyond_9(self): + self.firewall.sg_rules = self._fake_sg_rule() + self.firewall.ipset_chains = {'IPv4fake_sgid': TEST_IP_RANGE[5:]} + self.firewall.sg_members = {'fake_sgid': { + 'IPv4': TEST_IP_RANGE[:5], + 'IPv6': ['fe80::1']}} + self.firewall.pre_sg_members = {'fake_sgid': { + 'IPv4': TEST_IP_RANGE[5:], + 'IPv6': ['fe80::1']}} + port = self._fake_port() + self.firewall.prepare_port_filter(port) + calls = [ + mock.call.refresh_ipset_chain_by_name('IPv4fake_sgid', + TEST_IP_RANGE[:5], 'IPv4'), + mock.call.create_ipset_chain('IPv6fake_sgid', 'IPv6'), + mock.call.refresh_ipset_chain_by_name( + 'IPv6fake_sgid', ['fe80::1'], 'IPv6')] + + self.firewall.ipset.assert_has_calls(calls) diff --git a/neutron/tests/unit/test_security_groups_rpc.py b/neutron/tests/unit/test_security_groups_rpc.py index d26c904be..b52c50aab 100644 --- a/neutron/tests/unit/test_security_groups_rpc.py +++ b/neutron/tests/unit/test_security_groups_rpc.py @@ -1477,6 +1477,58 @@ CHAINS_2 = CHAINS_1 + '|i_port2|o_port2|s_port2' IPTABLES_ARG['chains'] = CHAINS_1 +IPSET_FILTER_1 = """# Generated by iptables_manager +*filter +:neutron-filter-top - [0:0] +:%(bn)s-(%(chains)s) - [0:0] +:%(bn)s-(%(chains)s) - [0:0] +:%(bn)s-(%(chains)s) - [0:0] +:%(bn)s-(%(chains)s) - [0:0] +:%(bn)s-(%(chains)s) - [0:0] +:%(bn)s-(%(chains)s) - [0:0] +:%(bn)s-(%(chains)s) - [0:0] +:%(bn)s-(%(chains)s) - [0:0] +:%(bn)s-(%(chains)s) - [0:0] +[0:0] -A FORWARD -j neutron-filter-top +[0:0] -A OUTPUT -j neutron-filter-top +[0:0] -A neutron-filter-top -j %(bn)s-local +[0:0] -A INPUT -j %(bn)s-INPUT +[0:0] -A OUTPUT -j %(bn)s-OUTPUT +[0:0] -A FORWARD -j %(bn)s-FORWARD +[0:0] -A %(bn)s-sg-fallback -j DROP +[0:0] -A %(bn)s-FORWARD %(physdev_mod)s --physdev-INGRESS tap_port1 \ +%(physdev_is_bridged)s -j %(bn)s-sg-chain +[0:0] -A %(bn)s-sg-chain %(physdev_mod)s --physdev-INGRESS tap_port1 \ +%(physdev_is_bridged)s -j %(bn)s-i_port1 +[0:0] -A %(bn)s-i_port1 -m state --state INVALID -j DROP +[0:0] -A %(bn)s-i_port1 -m state --state RELATED,ESTABLISHED -j RETURN +[0:0] -A %(bn)s-i_port1 -s 10.0.0.2/32 -p udp -m udp --sport 67 --dport 68 \ +-j RETURN +[0:0] -A %(bn)s-i_port1 -p tcp -m tcp --dport 22 -j RETURN +[0:0] -A %(bn)s-i_port1 -m set --match-set IPv4security_group1 src -j \ +RETURN +[0:0] -A %(bn)s-i_port1 -j %(bn)s-sg-fallback +[0:0] -A %(bn)s-FORWARD %(physdev_mod)s --physdev-EGRESS tap_port1 \ +%(physdev_is_bridged)s -j %(bn)s-sg-chain +[0:0] -A %(bn)s-sg-chain %(physdev_mod)s --physdev-EGRESS tap_port1 \ +%(physdev_is_bridged)s -j %(bn)s-o_port1 +[0:0] -A %(bn)s-INPUT %(physdev_mod)s --physdev-EGRESS tap_port1 \ +%(physdev_is_bridged)s -j %(bn)s-o_port1 +[0:0] -A %(bn)s-s_port1 -m mac --mac-source 12:34:56:78:9a:bc -s 10.0.0.3/32 \ +-j RETURN +[0:0] -A %(bn)s-s_port1 -j DROP +[0:0] -A %(bn)s-o_port1 -p udp -m udp --sport 68 --dport 67 -j RETURN +[0:0] -A %(bn)s-o_port1 -j %(bn)s-s_port1 +[0:0] -A %(bn)s-o_port1 -p udp -m udp --sport 67 --dport 68 -j DROP +[0:0] -A %(bn)s-o_port1 -m state --state INVALID -j DROP +[0:0] -A %(bn)s-o_port1 -m state --state RELATED,ESTABLISHED -j RETURN +[0:0] -A %(bn)s-o_port1 -j RETURN +[0:0] -A %(bn)s-o_port1 -j %(bn)s-sg-fallback +[0:0] -A %(bn)s-sg-chain -j ACCEPT +COMMIT +# Completed by iptables_manager +""" % IPTABLES_ARG + IPTABLES_FILTER_1 = """# Generated by iptables_manager *filter :neutron-filter-top - [0:0] @@ -1581,6 +1633,174 @@ COMMIT IPTABLES_ARG['chains'] = CHAINS_2 +IPSET_FILTER_2 = """# Generated by iptables_manager +*filter +:neutron-filter-top - [0:0] +:%(bn)s-(%(chains)s) - [0:0] +:%(bn)s-(%(chains)s) - [0:0] +:%(bn)s-(%(chains)s) - [0:0] +:%(bn)s-(%(chains)s) - [0:0] +:%(bn)s-(%(chains)s) - [0:0] +:%(bn)s-(%(chains)s) - [0:0] +:%(bn)s-(%(chains)s) - [0:0] +:%(bn)s-(%(chains)s) - [0:0] +:%(bn)s-(%(chains)s) - [0:0] +:%(bn)s-(%(chains)s) - [0:0] +:%(bn)s-(%(chains)s) - [0:0] +:%(bn)s-(%(chains)s) - [0:0] +[0:0] -A FORWARD -j neutron-filter-top +[0:0] -A OUTPUT -j neutron-filter-top +[0:0] -A neutron-filter-top -j %(bn)s-local +[0:0] -A INPUT -j %(bn)s-INPUT +[0:0] -A OUTPUT -j %(bn)s-OUTPUT +[0:0] -A FORWARD -j %(bn)s-FORWARD +[0:0] -A %(bn)s-sg-fallback -j DROP +[0:0] -A %(bn)s-FORWARD %(physdev_mod)s --physdev-INGRESS tap_port1 \ +%(physdev_is_bridged)s -j %(bn)s-sg-chain +[0:0] -A %(bn)s-sg-chain %(physdev_mod)s --physdev-INGRESS tap_port1 \ +%(physdev_is_bridged)s -j %(bn)s-i_port1 +[0:0] -A %(bn)s-i_port1 -m state --state INVALID -j DROP +[0:0] -A %(bn)s-i_port1 -m state --state RELATED,ESTABLISHED -j RETURN +[0:0] -A %(bn)s-i_port1 -s 10.0.0.2/32 -p udp -m udp --sport 67 --dport 68 \ +-j RETURN +[0:0] -A %(bn)s-i_port1 -p tcp -m tcp --dport 22 -j RETURN +[0:0] -A %(bn)s-i_port1 -m set --match-set IPv4security_group1 src -j \ +RETURN +[0:0] -A %(bn)s-i_port1 -j %(bn)s-sg-fallback +[0:0] -A %(bn)s-FORWARD %(physdev_mod)s --physdev-EGRESS tap_port1 \ +%(physdev_is_bridged)s -j %(bn)s-sg-chain +[0:0] -A %(bn)s-sg-chain %(physdev_mod)s --physdev-EGRESS tap_port1 \ +%(physdev_is_bridged)s -j %(bn)s-o_port1 +[0:0] -A %(bn)s-INPUT %(physdev_mod)s --physdev-EGRESS tap_port1 \ +%(physdev_is_bridged)s -j %(bn)s-o_port1 +[0:0] -A %(bn)s-s_port1 -m mac --mac-source 12:34:56:78:9a:bc -s 10.0.0.3/32 \ +-j RETURN +[0:0] -A %(bn)s-s_port1 -j DROP +[0:0] -A %(bn)s-o_port1 -p udp -m udp --sport 68 --dport 67 -j RETURN +[0:0] -A %(bn)s-o_port1 -j %(bn)s-s_port1 +[0:0] -A %(bn)s-o_port1 -p udp -m udp --sport 67 --dport 68 -j DROP +[0:0] -A %(bn)s-o_port1 -m state --state INVALID -j DROP +[0:0] -A %(bn)s-o_port1 -m state --state RELATED,ESTABLISHED -j RETURN +[0:0] -A %(bn)s-o_port1 -j RETURN +[0:0] -A %(bn)s-o_port1 -j %(bn)s-sg-fallback +[0:0] -A %(bn)s-FORWARD %(physdev_mod)s --physdev-INGRESS tap_port2 \ +%(physdev_is_bridged)s -j %(bn)s-sg-chain +[0:0] -A %(bn)s-sg-chain %(physdev_mod)s --physdev-INGRESS tap_port2 \ +%(physdev_is_bridged)s -j %(bn)s-i_port2 +[0:0] -A %(bn)s-i_port2 -m state --state INVALID -j DROP +[0:0] -A %(bn)s-i_port2 -m state --state RELATED,ESTABLISHED -j RETURN +[0:0] -A %(bn)s-i_port2 -s 10.0.0.2/32 -p udp -m udp --sport 67 --dport 68 \ +-j RETURN +[0:0] -A %(bn)s-i_port2 -p tcp -m tcp --dport 22 -j RETURN +[0:0] -A %(bn)s-i_port2 -m set --match-set IPv4security_group1 src -j \ +RETURN +[0:0] -A %(bn)s-i_port2 -j %(bn)s-sg-fallback +[0:0] -A %(bn)s-FORWARD %(physdev_mod)s --physdev-EGRESS tap_port2 \ +%(physdev_is_bridged)s -j %(bn)s-sg-chain +[0:0] -A %(bn)s-sg-chain %(physdev_mod)s --physdev-EGRESS tap_port2 \ +%(physdev_is_bridged)s -j %(bn)s-o_port2 +[0:0] -A %(bn)s-INPUT %(physdev_mod)s --physdev-EGRESS tap_port2 \ +%(physdev_is_bridged)s -j %(bn)s-o_port2 +[0:0] -A %(bn)s-s_port2 -m mac --mac-source 12:34:56:78:9a:bd -s 10.0.0.4/32 \ +-j RETURN +[0:0] -A %(bn)s-s_port2 -j DROP +[0:0] -A %(bn)s-o_port2 -p udp -m udp --sport 68 --dport 67 -j RETURN +[0:0] -A %(bn)s-o_port2 -j %(bn)s-s_port2 +[0:0] -A %(bn)s-o_port2 -p udp -m udp --sport 67 --dport 68 -j DROP +[0:0] -A %(bn)s-o_port2 -m state --state INVALID -j DROP +[0:0] -A %(bn)s-o_port2 -m state --state RELATED,ESTABLISHED -j RETURN +[0:0] -A %(bn)s-o_port2 -j RETURN +[0:0] -A %(bn)s-o_port2 -j %(bn)s-sg-fallback +[0:0] -A %(bn)s-sg-chain -j ACCEPT +COMMIT +# Completed by iptables_manager +""" % IPTABLES_ARG + +IPSET_FILTER_2_3 = """# Generated by iptables_manager +*filter +:neutron-filter-top - [0:0] +:%(bn)s-(%(chains)s) - [0:0] +:%(bn)s-(%(chains)s) - [0:0] +:%(bn)s-(%(chains)s) - [0:0] +:%(bn)s-(%(chains)s) - [0:0] +:%(bn)s-(%(chains)s) - [0:0] +:%(bn)s-(%(chains)s) - [0:0] +:%(bn)s-(%(chains)s) - [0:0] +:%(bn)s-(%(chains)s) - [0:0] +:%(bn)s-(%(chains)s) - [0:0] +:%(bn)s-(%(chains)s) - [0:0] +:%(bn)s-(%(chains)s) - [0:0] +:%(bn)s-(%(chains)s) - [0:0] +[0:0] -A FORWARD -j neutron-filter-top +[0:0] -A OUTPUT -j neutron-filter-top +[0:0] -A neutron-filter-top -j %(bn)s-local +[0:0] -A INPUT -j %(bn)s-INPUT +[0:0] -A OUTPUT -j %(bn)s-OUTPUT +[0:0] -A FORWARD -j %(bn)s-FORWARD +[0:0] -A %(bn)s-sg-fallback -j DROP +[0:0] -A %(bn)s-FORWARD %(physdev_mod)s --physdev-INGRESS tap_port1 \ +%(physdev_is_bridged)s -j %(bn)s-sg-chain +[0:0] -A %(bn)s-sg-chain %(physdev_mod)s --physdev-INGRESS tap_port1 \ +%(physdev_is_bridged)s -j %(bn)s-i_port1 +[0:0] -A %(bn)s-i_port1 -m state --state INVALID -j DROP +[0:0] -A %(bn)s-i_port1 -m state --state RELATED,ESTABLISHED -j RETURN +[0:0] -A %(bn)s-i_port1 -s 10.0.0.2/32 -p udp -m udp --sport 67 --dport 68 \ +-j RETURN +[0:0] -A %(bn)s-i_port1 -p tcp -m tcp --dport 22 -j RETURN +[0:0] -A %(bn)s-i_port1 -m set --match-set IPv4security_group1 src -j \ +RETURN +[0:0] -A %(bn)s-i_port1 -p icmp -j RETURN +[0:0] -A %(bn)s-i_port1 -j %(bn)s-sg-fallback +[0:0] -A %(bn)s-FORWARD %(physdev_mod)s --physdev-EGRESS tap_port1 \ +%(physdev_is_bridged)s -j %(bn)s-sg-chain +[0:0] -A %(bn)s-sg-chain %(physdev_mod)s --physdev-EGRESS tap_port1 \ +%(physdev_is_bridged)s -j %(bn)s-o_port1 +[0:0] -A %(bn)s-INPUT %(physdev_mod)s --physdev-EGRESS tap_port1 \ +%(physdev_is_bridged)s -j %(bn)s-o_port1 +[0:0] -A %(bn)s-s_port1 -m mac --mac-source 12:34:56:78:9a:bc -s 10.0.0.3/32 \ +-j RETURN +[0:0] -A %(bn)s-s_port1 -j DROP +[0:0] -A %(bn)s-o_port1 -p udp -m udp --sport 68 --dport 67 -j RETURN +[0:0] -A %(bn)s-o_port1 -j %(bn)s-s_port1 +[0:0] -A %(bn)s-o_port1 -p udp -m udp --sport 67 --dport 68 -j DROP +[0:0] -A %(bn)s-o_port1 -m state --state INVALID -j DROP +[0:0] -A %(bn)s-o_port1 -m state --state RELATED,ESTABLISHED -j RETURN +[0:0] -A %(bn)s-o_port1 -j RETURN +[0:0] -A %(bn)s-o_port1 -j %(bn)s-sg-fallback +[0:0] -A %(bn)s-FORWARD %(physdev_mod)s --physdev-INGRESS tap_port2 \ +%(physdev_is_bridged)s -j %(bn)s-sg-chain +[0:0] -A %(bn)s-sg-chain %(physdev_mod)s --physdev-INGRESS tap_port2 \ +%(physdev_is_bridged)s -j %(bn)s-i_port2 +[0:0] -A %(bn)s-i_port2 -m state --state INVALID -j DROP +[0:0] -A %(bn)s-i_port2 -m state --state RELATED,ESTABLISHED -j RETURN +[0:0] -A %(bn)s-i_port2 -s 10.0.0.2/32 -p udp -m udp --sport 67 --dport 68 \ +-j RETURN +[0:0] -A %(bn)s-i_port2 -p tcp -m tcp --dport 22 -j RETURN +[0:0] -A %(bn)s-i_port2 -m set --match-set IPv4security_group1 src -j \ +RETURN +[0:0] -A %(bn)s-i_port2 -p icmp -j RETURN +[0:0] -A %(bn)s-i_port2 -j %(bn)s-sg-fallback +[0:0] -A %(bn)s-FORWARD %(physdev_mod)s --physdev-EGRESS tap_port2 \ +%(physdev_is_bridged)s -j %(bn)s-sg-chain +[0:0] -A %(bn)s-sg-chain %(physdev_mod)s --physdev-EGRESS tap_port2 \ +%(physdev_is_bridged)s -j %(bn)s-o_port2 +[0:0] -A %(bn)s-INPUT %(physdev_mod)s --physdev-EGRESS tap_port2 \ +%(physdev_is_bridged)s -j %(bn)s-o_port2 +[0:0] -A %(bn)s-s_port2 -m mac --mac-source 12:34:56:78:9a:bd -s 10.0.0.4/32 \ +-j RETURN +[0:0] -A %(bn)s-s_port2 -j DROP +[0:0] -A %(bn)s-o_port2 -p udp -m udp --sport 68 --dport 67 -j RETURN +[0:0] -A %(bn)s-o_port2 -j %(bn)s-s_port2 +[0:0] -A %(bn)s-o_port2 -p udp -m udp --sport 67 --dport 68 -j DROP +[0:0] -A %(bn)s-o_port2 -m state --state INVALID -j DROP +[0:0] -A %(bn)s-o_port2 -m state --state RELATED,ESTABLISHED -j RETURN +[0:0] -A %(bn)s-o_port2 -j RETURN +[0:0] -A %(bn)s-o_port2 -j %(bn)s-sg-fallback +[0:0] -A %(bn)s-sg-chain -j ACCEPT +COMMIT +# Completed by iptables_manager +""" % IPTABLES_ARG + IPTABLES_FILTER_2 = """# Generated by iptables_manager *filter :neutron-filter-top - [0:0] @@ -2016,6 +2236,7 @@ class TestSecurityGroupAgentWithIptables(base.BaseTestCase): 'lock_path', '$state_path/lock') set_firewall_driver(self.FIREWALL_DRIVER) + cfg.CONF.set_override('enable_ipset', False, group='SECURITYGROUP') self.agent = sg_rpc.SecurityGroupAgentRpcMixin() self.agent.context = None @@ -2031,7 +2252,6 @@ class TestSecurityGroupAgentWithIptables(base.BaseTestCase): self.agent.init_firewall( defer_refresh_firewall=defer_refresh_firewall) - self.iptables = self.agent.firewall.iptables # TODO(jlibosva) Get rid of mocking iptables execute and mock out # firewall instead @@ -2307,6 +2527,58 @@ class TestSecurityGroupAgentEnhancedRpcWithIptables( self._verify_mock_calls() +class TestSecurityGroupAgentEnhancedIpsetWithIptables( + TestSecurityGroupAgentEnhancedRpcWithIptables): + def setUp(self, defer_refresh_firewall=False): + super(TestSecurityGroupAgentEnhancedIpsetWithIptables, self).setUp( + defer_refresh_firewall) + self.agent.firewall.enable_ipset = True + self.ipset = self.agent.firewall.ipset + self.ipset_execute = mock.patch.object(self.ipset, + "execute").start() + + def test_prepare_remove_port(self): + self.sg_info.return_value = self.devices_info1 + self._replay_iptables(IPSET_FILTER_1, IPTABLES_FILTER_V6_1) + self._replay_iptables(IPTABLES_FILTER_EMPTY, IPTABLES_FILTER_V6_EMPTY) + + self.agent.prepare_devices_filter(['tap_port1']) + self.agent.remove_devices_filter(['tap_port1']) + + self._verify_mock_calls() + + def test_security_group_member_updated(self): + self.sg_info.return_value = self.devices_info1 + self._replay_iptables(IPSET_FILTER_1, IPTABLES_FILTER_V6_1) + self._replay_iptables(IPSET_FILTER_1, IPTABLES_FILTER_V6_1) + self._replay_iptables(IPSET_FILTER_2, IPTABLES_FILTER_V6_2) + self._replay_iptables(IPSET_FILTER_2, IPTABLES_FILTER_V6_2) + self._replay_iptables(IPSET_FILTER_1, IPTABLES_FILTER_V6_1) + self._replay_iptables(IPTABLES_FILTER_EMPTY, IPTABLES_FILTER_V6_EMPTY) + + self.agent.prepare_devices_filter(['tap_port1']) + self.sg_info.return_value = self.devices_info2 + self.agent.security_groups_member_updated(['security_group1']) + self.agent.prepare_devices_filter(['tap_port2']) + self.sg_info.return_value = self.devices_info1 + self.agent.security_groups_member_updated(['security_group1']) + self.agent.remove_devices_filter(['tap_port2']) + self.agent.remove_devices_filter(['tap_port1']) + + self._verify_mock_calls() + + def test_security_group_rule_updated(self): + self.sg_info.return_value = self.devices_info2 + self._replay_iptables(IPSET_FILTER_2, IPTABLES_FILTER_V6_2) + self._replay_iptables(IPSET_FILTER_2_3, IPTABLES_FILTER_V6_2) + + self.agent.prepare_devices_filter(['tap_port1', 'tap_port3']) + self.sg_info.return_value = self.devices_info3 + self.agent.security_groups_rule_updated(['security_group1']) + + self._verify_mock_calls() + + class SGNotificationTestMixin(): def test_security_group_rule_updated(self): name = 'webservers' diff --git a/setup.cfg b/setup.cfg index d3aa4329d..ff037f75a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,6 +38,7 @@ data_files = etc/neutron/rootwrap.d/debug.filters etc/neutron/rootwrap.d/dhcp.filters etc/neutron/rootwrap.d/iptables-firewall.filters + etc/neutron/rootwrap.d/ipset-firewall.filters etc/neutron/rootwrap.d/l3.filters etc/neutron/rootwrap.d/lbaas-haproxy.filters etc/neutron/rootwrap.d/linuxbridge-plugin.filters