# Copyright 2011 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # All Rights Reserved. # Copyright (c) 2011 Citrix Systems, Inc. # # 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 oslo_utils import importutils import nova.conf from nova import context from nova.network import linux_net from nova import objects from nova import utils from nova.virt import netutils LOG = logging.getLogger(__name__) CONF = nova.conf.CONF def load_driver(default, *args, **kwargs): fw_class = importutils.import_class(CONF.firewall_driver or default) return fw_class(*args, **kwargs) class FirewallDriver(object): """Firewall Driver base class. Defines methods that any driver providing security groups should implement. """ def prepare_instance_filter(self, instance, network_info): """Prepare filters for the instance. At this point, the instance isn't running yet. """ raise NotImplementedError() def filter_defer_apply_on(self): """Defer application of IPTables rules.""" pass def filter_defer_apply_off(self): """Turn off deferral of IPTables rules and apply the rules now.""" pass def unfilter_instance(self, instance, network_info): """Stop filtering instance.""" raise NotImplementedError() def apply_instance_filter(self, instance, network_info): """Apply instance filter. Once this method returns, the instance should be firewalled appropriately. This method should as far as possible be a no-op. It's vastly preferred to get everything set up in prepare_instance_filter. """ raise NotImplementedError() def refresh_security_group_rules(self, security_group_id): """Refresh security group rules from data store Gets called when a rule has been added to or removed from the security group. """ raise NotImplementedError() def refresh_instance_security_rules(self, instance): """Refresh security group rules from data store Gets called when an instance gets added to or removed from the security group the instance is a member of or if the group gains or loses a rule. """ raise NotImplementedError() def setup_basic_filtering(self, instance, network_info): """Create rules to block spoofing and allow dhcp. This gets called when spawning an instance, before :py:meth:`prepare_instance_filter`. """ raise NotImplementedError() def instance_filter_exists(self, instance, network_info): """Check nova-instance-instance-xxx exists.""" raise NotImplementedError() class IptablesFirewallDriver(FirewallDriver): """Driver which enforces security groups through iptables rules.""" def __init__(self, **kwargs): self.iptables = linux_net.iptables_manager self.instance_info = {} # Flags for DHCP request rule self.dhcp_create = False self.dhcp_created = False self.iptables.ipv4['filter'].add_chain('sg-fallback') self.iptables.ipv4['filter'].add_rule('sg-fallback', '-j DROP') self.iptables.ipv6['filter'].add_chain('sg-fallback') self.iptables.ipv6['filter'].add_rule('sg-fallback', '-j DROP') def setup_basic_filtering(self, instance, network_info): pass def apply_instance_filter(self, instance, network_info): """No-op. Everything is done in prepare_instance_filter.""" pass def filter_defer_apply_on(self): self.iptables.defer_apply_on() def filter_defer_apply_off(self): self.iptables.defer_apply_off() def unfilter_instance(self, instance, network_info): if self.instance_info.pop(instance.id, None): self.remove_filters_for_instance(instance) self.iptables.apply() else: LOG.info('Attempted to unfilter instance which is not filtered', instance=instance) def prepare_instance_filter(self, instance, network_info): self.instance_info[instance.id] = (instance, network_info) ipv4_rules, ipv6_rules = self.instance_rules(instance, network_info) self.add_filters_for_instance(instance, network_info, ipv4_rules, ipv6_rules) LOG.debug('Filters added to instance: %s', instance.id, instance=instance) # Ensure that DHCP request rule is updated if necessary if (self.dhcp_create and not self.dhcp_created): self.iptables.ipv4['filter'].add_rule( 'INPUT', '-s 0.0.0.0/32 -d 255.255.255.255/32 ' '-p udp -m udp --sport 68 --dport 67 -j ACCEPT') self.iptables.ipv4['filter'].add_rule( 'FORWARD', '-s 0.0.0.0/32 -d 255.255.255.255/32 ' '-p udp -m udp --sport 68 --dport 67 -j ACCEPT') self.dhcp_created = True self.iptables.apply() def _create_filter(self, ips, chain_name): return ['-d %s -j $%s' % (ip, chain_name) for ip in ips] def _get_subnets(self, network_info, version): subnets = [] for vif in network_info: if 'network' in vif and 'subnets' in vif['network']: for subnet in vif['network']['subnets']: if subnet['version'] == version: subnets.append(subnet) return subnets def _filters_for_instance(self, chain_name, network_info): """Creates a rule corresponding to each ip that defines a jump to the corresponding instance - chain for all the traffic destined to that ip. """ v4_subnets = self._get_subnets(network_info, 4) v6_subnets = self._get_subnets(network_info, 6) ips_v4 = [ip['address'] for subnet in v4_subnets for ip in subnet['ips']] ipv4_rules = self._create_filter(ips_v4, chain_name) ipv6_rules = ips_v6 = [] if CONF.use_ipv6: if v6_subnets: ips_v6 = [ip['address'] for subnet in v6_subnets for ip in subnet['ips']] ipv6_rules = self._create_filter(ips_v6, chain_name) return ipv4_rules, ipv6_rules def _add_filters(self, chain_name, ipv4_rules, ipv6_rules): for rule in ipv4_rules: self.iptables.ipv4['filter'].add_rule(chain_name, rule) if CONF.use_ipv6: for rule in ipv6_rules: self.iptables.ipv6['filter'].add_rule(chain_name, rule) def add_filters_for_instance(self, instance, network_info, inst_ipv4_rules, inst_ipv6_rules): chain_name = self._instance_chain_name(instance) if CONF.use_ipv6: self.iptables.ipv6['filter'].add_chain(chain_name) self.iptables.ipv4['filter'].add_chain(chain_name) ipv4_rules, ipv6_rules = self._filters_for_instance(chain_name, network_info) self._add_filters('local', ipv4_rules, ipv6_rules) self._add_filters(chain_name, inst_ipv4_rules, inst_ipv6_rules) def remove_filters_for_instance(self, instance): chain_name = self._instance_chain_name(instance) self.iptables.ipv4['filter'].remove_chain(chain_name) if CONF.use_ipv6: self.iptables.ipv6['filter'].remove_chain(chain_name) def _instance_chain_name(self, instance): return 'inst-%s' % (instance.id,) def _do_basic_rules(self, ipv4_rules, ipv6_rules, network_info): # Always drop invalid packets ipv4_rules += ['-m state --state ' 'INVALID -j DROP'] ipv6_rules += ['-m state --state ' 'INVALID -j DROP'] # Allow established connections ipv4_rules += ['-m state --state ESTABLISHED,RELATED -j ACCEPT'] ipv6_rules += ['-m state --state ESTABLISHED,RELATED -j ACCEPT'] def _do_dhcp_rules(self, ipv4_rules, network_info): v4_subnets = self._get_subnets(network_info, 4) dhcp_servers = [subnet.get_meta('dhcp_server') for subnet in v4_subnets if subnet.get_meta('dhcp_server')] for dhcp_server in dhcp_servers: if dhcp_server: ipv4_rules.append('-s %s -p udp --sport 67 --dport 68 ' '-j ACCEPT' % (dhcp_server,)) self.dhcp_create = True def _do_project_network_rules(self, ipv4_rules, ipv6_rules, network_info): v4_subnets = self._get_subnets(network_info, 4) v6_subnets = self._get_subnets(network_info, 6) cidrs = [subnet['cidr'] for subnet in v4_subnets] for cidr in cidrs: ipv4_rules.append('-s %s -j ACCEPT' % (cidr,)) if CONF.use_ipv6: cidrv6s = [subnet['cidr'] for subnet in v6_subnets] for cidrv6 in cidrv6s: ipv6_rules.append('-s %s -j ACCEPT' % (cidrv6,)) def _do_ra_rules(self, ipv6_rules, network_info): v6_subnets = self._get_subnets(network_info, 6) gateways_v6 = [subnet['gateway']['address'] for subnet in v6_subnets] for gateway_v6 in gateways_v6: ipv6_rules.append( '-s %s/128 -p icmpv6 -j ACCEPT' % (gateway_v6,)) def _build_icmp_rule(self, rule, version): icmp_type = rule.from_port icmp_code = rule.to_port if icmp_type == -1: icmp_type_arg = None else: icmp_type_arg = '%s' % icmp_type if not icmp_code == -1: icmp_type_arg += '/%s' % icmp_code if icmp_type_arg: if version == 4: return ['-m', 'icmp', '--icmp-type', icmp_type_arg] elif version == 6: return ['-m', 'icmp6', '--icmpv6-type', icmp_type_arg] # return empty list if icmp_type == -1 return [] def _build_tcp_udp_rule(self, rule, version): if rule.from_port == rule.to_port: return ['--dport', '%s' % (rule.from_port,)] else: return ['-m', 'multiport', '--dports', '%s:%s' % (rule.from_port, rule.to_port)] def instance_rules(self, instance, network_info): ctxt = context.get_admin_context() if isinstance(instance, dict): # NOTE(danms): allow old-world instance objects from # unconverted callers; all we need is instance.uuid below instance = objects.Instance._from_db_object( ctxt, objects.Instance(), instance, []) ipv4_rules = [] ipv6_rules = [] # Initialize with basic rules self._do_basic_rules(ipv4_rules, ipv6_rules, network_info) # Set up rules to allow traffic to/from DHCP server self._do_dhcp_rules(ipv4_rules, network_info) # Allow project network traffic if CONF.allow_same_net_traffic: self._do_project_network_rules(ipv4_rules, ipv6_rules, network_info) # We wrap these in CONF.use_ipv6 because they might cause # a DB lookup. The other ones are just list operations, so # they're not worth the clutter. if CONF.use_ipv6: # Allow RA responses self._do_ra_rules(ipv6_rules, network_info) # then, security group chains and rules rules = objects.SecurityGroupRuleList.get_by_instance(ctxt, instance) for rule in rules: if not rule.cidr: version = 4 else: version = netutils.get_ip_version(rule.cidr) if version == 4: fw_rules = ipv4_rules else: fw_rules = ipv6_rules protocol = rule.protocol if protocol: protocol = rule.protocol.lower() if version == 6 and protocol == 'icmp': protocol = 'icmpv6' args = ['-j ACCEPT'] if protocol: args += ['-p', protocol] if protocol in ['udp', 'tcp']: args += self._build_tcp_udp_rule(rule, version) elif protocol == 'icmp': args += self._build_icmp_rule(rule, version) if rule.cidr: args += ['-s', str(rule.cidr)] fw_rules += [' '.join(args)] else: if rule.grantee_group: insts = objects.InstanceList.get_by_security_group( ctxt, rule.grantee_group) for inst in insts: if inst.info_cache.deleted: LOG.debug('ignoring deleted cache') continue nw_info = inst.get_network_info() ips = [ip['address'] for ip in nw_info.fixed_ips() if ip['version'] == version] LOG.debug('ips: %r', ips, instance=inst) for ip in ips: subrule = args + ['-s %s' % ip] fw_rules += [' '.join(subrule)] ipv4_rules += ['-j $sg-fallback'] ipv6_rules += ['-j $sg-fallback'] LOG.debug('Security Group Rules %s translated to ipv4: %r, ipv6: %r', list(rules), ipv4_rules, ipv6_rules, instance=instance) return ipv4_rules, ipv6_rules def instance_filter_exists(self, instance, network_info): pass def refresh_security_group_rules(self, security_group): self.do_refresh_security_group_rules(security_group) self.iptables.apply() def refresh_instance_security_rules(self, instance): self.do_refresh_instance_rules(instance) self.iptables.apply() @utils.synchronized('iptables', external=True) def _inner_do_refresh_rules(self, instance, network_info, ipv4_rules, ipv6_rules): chain_name = self._instance_chain_name(instance) if not self.iptables.ipv4['filter'].has_chain(chain_name): LOG.info('instance chain %s disappeared during refresh, skipping', chain_name, instance=instance) return self.remove_filters_for_instance(instance) self.add_filters_for_instance(instance, network_info, ipv4_rules, ipv6_rules) def do_refresh_security_group_rules(self, security_group): id_list = self.instance_info.keys() for instance_id in id_list: try: instance, network_info = self.instance_info[instance_id] except KeyError: # NOTE(danms): instance cache must have been modified, # ignore this deleted instance and move on continue ipv4_rules, ipv6_rules = self.instance_rules(instance, network_info) self._inner_do_refresh_rules(instance, network_info, ipv4_rules, ipv6_rules) def do_refresh_instance_rules(self, instance): _instance, network_info = self.instance_info[instance.id] ipv4_rules, ipv6_rules = self.instance_rules(instance, network_info) self._inner_do_refresh_rules(instance, network_info, ipv4_rules, ipv6_rules) class NoopFirewallDriver(object): """Firewall driver which just provides No-op methods.""" def __init__(self, *args, **kwargs): pass def _noop(self, *args, **kwargs): pass def __getattr__(self, key): return self._noop def instance_filter_exists(self, instance, network_info): return True