neutron-fwaas/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/openvswitch_firewall/firewall.py

1018 lines
40 KiB
Python

# Copyright 2015
# 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 netaddr
from neutron_lib import constants as lib_const
from oslo_log import log as logging
from oslo_utils import netutils
from neutron.agent import firewall
from neutron.plugins.ml2.drivers.openvswitch.agent.common import constants \
as ovs_consts
from neutron_fwaas.services.firewall.service_drivers.agents.drivers.linux.l2\
import driver_base
from neutron_fwaas.services.firewall.service_drivers.agents.drivers.linux.l2.\
openvswitch_firewall import constants as fwaas_ovs_consts
from neutron_fwaas.services.firewall.service_drivers.agents.drivers.linux.l2.\
openvswitch_firewall import exceptions
from neutron_fwaas.services.firewall.service_drivers.agents.drivers.linux.l2.\
openvswitch_firewall import rules
LOG = logging.getLogger(__name__)
ACTION_ALLOW = 'allow'
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver.
def _replace_register(flow_params, register_number, register_value):
"""Replace value from flows to given register number
'register_value' key in dictionary will be replaced by register number
given by 'register_number'
:param flow_params: Dictionary containing defined flows
:param register_number: The number of register where value will be stored
:param register_value: Key to be replaced by register number
"""
try:
reg_port = flow_params[register_value]
del flow_params[register_value]
flow_params['reg{:d}'.format(register_number)] = reg_port
except KeyError:
pass
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver that
# differs only in constants REG_PORT/REG_NET.
def create_reg_numbers(flow_params):
"""Replace reg_(port|net) values with defined register numbers"""
_replace_register(flow_params, fwaas_ovs_consts.REG_PORT, 'reg_port')
_replace_register(flow_params, fwaas_ovs_consts.REG_NET, 'reg_net')
class FirewallGroup(object):
def __init__(self, id_):
self.id = id_
self.ingress_rules = []
self.egress_rules = []
self.members = {}
self.ports = set()
def update_rules(self, ingress_rules, egress_rules):
"""Update firewall group with ingress/egress rules.
If a rule has a protocol field, it is normalized to a number
here in order to ease later processing.
"""
def _translate_protocol_to_number(rule):
protocol = rule.get('protocol')
if protocol is not None:
if protocol.isdigit():
rule['protocol'] = int(protocol)
elif (rule.get('ethertype') == lib_const.IPv6 and
protocol == lib_const.PROTO_NAME_ICMP):
rule['protocol'] = lib_const.PROTO_NUM_IPV6_ICMP
else:
rule['protocol'] = lib_const.IP_PROTOCOL_MAP.get(
protocol, protocol)
return rule
self.ingress_rules = [_translate_protocol_to_number(ir)
for ir in ingress_rules]
self.egress_rules = [_translate_protocol_to_number(er)
for er in egress_rules]
def get_ethertype_filtered_addresses(self, ethertype,
exclude_addresses=None):
exclude_addresses = set(exclude_addresses if exclude_addresses else [])
group_addresses = set(self.members.get(ethertype, []))
return list(group_addresses - exclude_addresses)
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver that
# differs only in firewall groups list field name
class OFPort(object):
def __init__(self, port_dict, ovs_port, vlan_tag):
self.id = port_dict['device']
self.vlan_tag = vlan_tag
self.mac = ovs_port.vif_mac
self.lla_address = str(netutils.get_ipv6_addr_by_EUI64(
lib_const.IPv6_LLA_PREFIX, self.mac))
self.ofport = ovs_port.ofport
self.fw_group = None
self.fixed_ips = port_dict.get('fixed_ips', [])
self.neutron_port_dict = port_dict.copy()
self.allowed_pairs_v4 = self._get_allowed_pairs(port_dict, version=4)
self.allowed_pairs_v6 = self._get_allowed_pairs(port_dict, version=6)
@staticmethod
def _get_allowed_pairs(port_dict, version):
aap_dict = port_dict.get('allowed_address_pairs', set())
return {(aap['mac_address'], aap['ip_address']) for aap in aap_dict
if netaddr.IPNetwork(aap['ip_address']).version == version}
@property
def all_allowed_macs(self):
macs = {item[0] for item in self.allowed_pairs_v4.union(
self.allowed_pairs_v6)}
macs.add(self.mac)
return macs
@property
def ipv4_addresses(self):
return [ip_addr for ip_addr in
[fixed_ip['ip_address'] for fixed_ip in self.fixed_ips]
if netaddr.IPAddress(ip_addr).version == 4]
@property
def ipv6_addresses(self):
return [ip_addr for ip_addr in
[fixed_ip['ip_address'] for fixed_ip in self.fixed_ips]
if netaddr.IPAddress(ip_addr).version == 6]
def update(self, port_dict):
self.allowed_pairs_v4 = self._get_allowed_pairs(port_dict,
version=4)
self.allowed_pairs_v6 = self._get_allowed_pairs(port_dict,
version=6)
# Neighbour discovery uses LLA
self.allowed_pairs_v6.add((self.mac, self.lla_address))
self.fixed_ips = port_dict.get('fixed_ips', [])
self.neutron_port_dict = port_dict.copy()
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver that
# differs in methods name [s/sg/fwg] and update_rules method.
class FWGPortMap(object):
def __init__(self):
self.ports = {}
self.fw_groups = {}
# Maps port_id to ofport number
self.unfiltered = {}
def get_fwg(self, fwg_id):
return self.fw_groups.get(fwg_id, None)
def get_or_create_fwg(self, fwg_id):
fw_group = self.get_fwg(fwg_id)
if not fw_group:
fw_group = FirewallGroup(fwg_id)
self.fw_groups[fwg_id] = fw_group
return fw_group
def delete_fwg(self, fwg_id):
del self.fw_groups[fwg_id]
# XXX NOTE(ivasilevskaya) couldn't find any logical definition why
# firewall_group should come as 3rd argument instead of adding fwg_id
# to port_dict. Removed in favor of SG api
def create_port(self, port, port_dict):
self.ports[port.id] = port
self.update_port(port, port_dict)
# XXX NOTE(ivasilevskaya) couldn't find any logical definition why
# firewall_group should come as 3rd argument instead of adding fwg_id
# to port_dict. Removed in favor of SG api
def update_port(self, port, port_dict):
for fw_group in self.fw_groups.values():
fw_group.ports.discard(port)
fw_group = self.get_or_create_fwg(port_dict['firewall_group'])
port.fw_group = fw_group
fw_group.ports.add(port)
port.update(port_dict)
def remove_port(self, port):
if port.fw_group:
port.fw_group.ports.discard(port)
del self.ports[port.id]
def update_rules(self, fwg_id, ingress_rules, egress_rules):
fw_group = self.get_or_create_fwg(fwg_id)
fw_group.update_rules(ingress_rules, egress_rules)
def update_members(self, fwg_id, members):
fw_group = self.get_or_create_fwg(fwg_id)
fw_group.members = members
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver that
# doesn't have a conjunction manager because no remote_group_id concept is
# applicable to firewall groups
class OVSFirewallDriver(driver_base.FirewallL2DriverBase):
REQUIRED_PROTOCOLS = [
ovs_consts.OPENFLOW10,
ovs_consts.OPENFLOW11,
ovs_consts.OPENFLOW12,
ovs_consts.OPENFLOW13,
ovs_consts.OPENFLOW14,
]
provides_arp_spoofing_protection = True
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver.
# This driver won't have any conj_manager logic because there is no concept
# of remote_group_id for firewall groups (that I know of at least)
def __init__(self, agent_api, sg_with_ovs=False):
"""Initialize object"""
integration_bridge = agent_api.request_int_br()
self.int_br = self.initialize_bridge(integration_bridge)
self.fwg_port_map = FWGPortMap()
self.fwg_to_delete = set()
self._deferred = False
self.sg_with_ovs = sg_with_ovs
self._drop_all_unmatched_flows()
self._initialize_third_party_tables()
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
def _accept_flow(self, **flow):
for f in rules.create_accept_flows(flow, self.sg_with_ovs):
self._add_flow(**f)
def _drop_flow(self, **flow):
for f in rules.create_drop_flows(flow):
self._add_flow(**f)
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
def _add_flow(self, **kwargs):
dl_type = kwargs.get('dl_type')
create_reg_numbers(kwargs)
if isinstance(dl_type, int):
kwargs['dl_type'] = "0x{:04x}".format(dl_type)
if self._deferred:
self.int_br.add_flow(**kwargs)
else:
self.int_br.br.add_flow(**kwargs)
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
def _delete_flows(self, **kwargs):
create_reg_numbers(kwargs)
if self._deferred:
self.int_br.delete_flows(**kwargs)
else:
self.int_br.br.delete_flows(**kwargs)
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
def _strict_delete_flow(self, **kwargs):
"""Delete given flow right away even if bridge is deferred.
Delete command will use strict delete.
"""
create_reg_numbers(kwargs)
self.int_br.br.delete_flows(strict=True, **kwargs)
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
@staticmethod
def initialize_bridge(int_br):
int_br.add_protocols(*OVSFirewallDriver.REQUIRED_PROTOCOLS)
return int_br.deferred(full_ordered=True)
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver,
# differs in constants
def _drop_all_unmatched_flows(self):
for table in fwaas_ovs_consts.OVS_FIREWALL_TABLES:
if (table == fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE and
self.sg_with_ovs):
continue
self.int_br.br.add_flow(table=table, priority=0, actions='drop')
def _initialize_third_party_tables(self):
self.int_br.br.add_flow(
table=ovs_consts.ACCEPTED_EGRESS_TRAFFIC_TABLE,
priority=1,
actions='normal')
for table in (ovs_consts.ACCEPTED_INGRESS_TRAFFIC_TABLE,
ovs_consts.DROPPED_TRAFFIC_TABLE):
self.int_br.br.add_flow(
table=table, priority=0, actions='drop')
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
def get_ovs_port(self, port_id):
ovs_port = self.int_br.br.get_vif_port_by_id(port_id)
if not ovs_port:
raise exceptions.OVSFWaaSPortNotFound(port_id=port_id)
return ovs_port
def _get_port_vlan_tag(self, port):
vlan_tag = port.get('lvlan', None)
if not vlan_tag:
raise exceptions.OVSFWaaSTagNotFound(port_id=port['device'])
return vlan_tag
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
def get_ofport(self, port):
port_id = port['device']
return self.fwg_port_map.ports.get(port_id)
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver,
# self.sg_port_map -> self.fwg_port_map
def get_or_create_ofport(self, port):
"""Get ofport specified by port['device'], checking and reflecting
ofport changes.
If ofport is nonexistent, create and return one.
"""
port_id = port['device']
ovs_port = self.get_ovs_port(port_id)
try:
of_port = self.fwg_port_map.ports[port_id]
except KeyError:
port_vlan_id = self._get_port_vlan_tag(port)
of_port = OFPort(port, ovs_port, port_vlan_id)
self.fwg_port_map.create_port(of_port, port)
else:
if of_port.ofport != ovs_port.ofport:
self.fwg_port_map.remove_port(of_port)
of_port = OFPort(port, ovs_port, of_port.vlan_tag)
self.fwg_port_map.update_port(of_port, port)
return of_port
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
def is_port_managed(self, port):
return port['device'] in self.fwg_port_map.ports
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
def prepare_port_filter(self, port):
# NOTE(annp): port no security should be handled by security group in
# co-existence mode, otherwise(standalone mode) fwg will handle it.
if not firewall.port_sec_enabled(port) and not self.sg_with_ovs:
self._initialize_egress_no_port_security(port)
return
old_of_port = self.get_ofport(port)
# Make sure delete old allow_address_pair MACs because
# allow_address_pair MACs will be updated in
# self.get_or_create_ofport(port)
if old_of_port:
LOG.error("Initializing port %s that was already "
"initialized.",
port['device'])
self.delete_all_port_flows(old_of_port)
of_port = self.get_or_create_ofport(port)
self.initialize_port_flows(of_port)
self.add_flows_from_rules(of_port)
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
def update_port_filter(self, port):
"""Update rules for given port
Current existing filtering rules are removed and new ones are generated
based on current loaded firewall group rules and members.
Note: port no security should be handled by security group in
co-existence mode, otherwise fwg will handle it.
"""
if not firewall.port_sec_enabled(port) and not self.sg_with_ovs:
self.remove_port_filter(port)
self._initialize_egress_no_port_security(port)
return
elif not self.is_port_managed(port):
if not self.sg_with_ovs:
self._remove_egress_no_port_security(port['device'])
self.prepare_port_filter(port)
return
old_of_port = self.get_ofport(port)
of_port = self.get_or_create_ofport(port)
# TODO(jlibosva): Handle firewall blink
self.delete_all_port_flows(old_of_port)
self.initialize_port_flows(of_port)
self.add_flows_from_rules(of_port)
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver,
# sg_port_map -> fwg_port_map
def remove_port_filter(self, port):
"""Remove port from firewall
All flows related to this port are removed from ovs. Port is also
removed from ports managed by this firewall.
"""
if self.is_port_managed(port):
of_port = self.get_ofport(port)
self.delete_all_port_flows(of_port)
self.fwg_port_map.remove_port(of_port)
self._schedule_fwg_deletion_maybe(of_port.fw_group.id)
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
# with ingress\egress rules arguments instead of single rules
def update_firewall_group_rules(self, fwg_id, ingress_rules, egress_rules):
self.fwg_port_map.update_rules(fwg_id, ingress_rules, egress_rules)
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
# with sg_port_map -> fwg_port_map
def _schedule_fwg_deletion_maybe(self, fwg_id):
"""Schedule possible deletion of the given firewall group.
This function must be called when the number of ports
associated to fwg_id drops to zero, as it isn't possible
to know FWG deletions from agents due to RPC API design.
"""
fwg_group = self.fwg_port_map.get_or_create_fwg(fwg_id)
if not fwg_group.members or not fwg_group.ports:
self.fwg_to_delete.add(fwg_id)
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
# with sg_port_map -> fwg_port_map
def _cleanup_stale_fwg(self):
fwg_to_delete = self.fwg_to_delete
self.fwg_to_delete = set()
for fwg_id in fwg_to_delete:
fw_group = self.fwg_port_map.get_fwg(fwg_id)
if fw_group.members and fw_group.ports:
# firewall group is still in use
continue
self.fwg_port_map.delete_fwg(fwg_id)
def process_trusted_ports(self, ports):
"""Pass packets from these ports directly to ingress pipeline."""
if self.sg_with_ovs:
return
for port in ports:
self._initialize_egress_no_port_security(port)
def remove_trusted_ports(self, port_ids):
if self.sg_with_ovs:
return
for port_id in port_ids:
self._remove_egress_no_port_security(port_id)
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
def filter_defer_apply_on(self):
self._deferred = True
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
def filter_defer_apply_off(self):
if self._deferred:
self._cleanup_stale_fwg()
self.int_br.apply_flows()
self._deferred = False
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
# with sg_port_map -> fwg_port_map
@property
def ports(self):
return {id_: port.neutron_port_dict
for id_, port in self.fwg_port_map.ports.items()}
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
# which differs in constants (table numbers)
def initialize_port_flows(self, port):
"""Set base flows for port
:param port: OFPort instance
"""
# Identify egress flow
self._add_flow(
table=ovs_consts.TRANSIENT_TABLE,
priority=105,
in_port=port.ofport,
actions='set_field:{:d}->reg{:d},'
'set_field:{:d}->reg{:d},'
'resubmit(,{:d})'.format(
port.ofport,
fwaas_ovs_consts.REG_PORT,
port.vlan_tag,
fwaas_ovs_consts.REG_NET,
fwaas_ovs_consts.FW_BASE_EGRESS_TABLE)
)
# Identify ingress flows after egress filtering
for mac_addr in port.all_allowed_macs:
self._add_flow(
table=ovs_consts.TRANSIENT_TABLE,
priority=95,
dl_dst=mac_addr,
dl_vlan='0x%x' % port.vlan_tag,
actions='set_field:{:d}->reg{:d},'
'set_field:{:d}->reg{:d},'
'strip_vlan,resubmit(,{:d})'.format(
port.ofport,
fwaas_ovs_consts.REG_PORT,
port.vlan_tag,
fwaas_ovs_consts.REG_NET,
fwaas_ovs_consts.FW_BASE_INGRESS_TABLE),
)
self._initialize_egress(port)
self._initialize_ingress(port)
def _fwaas_process_colocated_ingress(self, port):
for mac_addr in port.all_allowed_macs:
self._add_flow(
table=ovs_consts.ACCEPT_OR_INGRESS_TABLE,
priority=105,
dl_dst=mac_addr,
reg_net=port.vlan_tag,
actions='set_field:{:d}->reg{:d},resubmit(,{:d})'.format(
port.ofport,
fwaas_ovs_consts.REG_PORT,
fwaas_ovs_consts.FW_BASE_INGRESS_TABLE),
)
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
# which differs in constants (table numbers)
def _initialize_egress_ipv6_icmp(self, port):
for icmp_type in firewall.ICMPV6_ALLOWED_EGRESS_TYPES:
self._add_flow(
table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE,
priority=95,
in_port=port.ofport,
reg_port=port.ofport,
dl_type=lib_const.ETHERTYPE_IPV6,
nw_proto=lib_const.PROTO_NUM_IPV6_ICMP,
icmp_type=icmp_type,
actions='normal')
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
# which differs in constants (table numbers) and exception classes
def _initialize_egress_no_port_security(self, port):
port_id = port['device']
try:
ovs_port = self.get_ovs_port(port_id)
vlan_tag = self._get_port_vlan_tag(port)
except exceptions.OVSFWaaSTagNotFound:
# It's a patch port, don't set anything
return
except exceptions.OVSFWaaSPortNotFound as not_found_e:
LOG.error("Initializing unfiltered port %(port_id)s that does not "
"exist in ovsdb: %(err)s.",
{'port_id': port_id,
'err': not_found_e})
return
self.fwg_port_map.unfiltered[port_id] = ovs_port.ofport
self._add_flow(
table=ovs_consts.TRANSIENT_TABLE,
priority=100,
in_port=ovs_port.ofport,
actions='set_field:%d->reg%d,'
'set_field:%d->reg%d,'
'resubmit(,%d)' % (
ovs_port.ofport,
fwaas_ovs_consts.REG_PORT,
vlan_tag,
fwaas_ovs_consts.REG_NET,
fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE)
)
self._add_flow(
table=fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE,
priority=80,
reg_port=ovs_port.ofport,
actions='normal',
)
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
# which differs in constants (table numbers)
def _remove_egress_no_port_security(self, port_id):
try:
ofport = self.fwg_port_map.unfiltered[port_id]
except KeyError:
LOG.debug("Port %s is not handled by the firewall.", port_id)
return
self._delete_flows(
table=ovs_consts.TRANSIENT_TABLE,
in_port=ofport
)
self._delete_flows(
table=fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE,
reg_port=ofport
)
del self.fwg_port_map.unfiltered[port_id]
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
# which differs in constants (table numbers)
def _initialize_egress(self, port):
"""Identify egress traffic and send it to egress base"""
self._initialize_egress_ipv6_icmp(port)
# Apply mac/ip pairs for IPv4
allowed_pairs = port.allowed_pairs_v4.union(
{(port.mac, ip_addr) for ip_addr in port.ipv4_addresses})
for mac_addr, ip_addr in allowed_pairs:
self._add_flow(
table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE,
priority=95,
in_port=port.ofport,
reg_port=port.ofport,
dl_src=mac_addr,
dl_type=lib_const.ETHERTYPE_ARP,
arp_spa=ip_addr,
actions='normal'
)
self._add_flow(
table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE,
priority=65,
reg_port=port.ofport,
ct_state=fwaas_ovs_consts.OF_STATE_NOT_TRACKED,
dl_type=lib_const.ETHERTYPE_IP,
in_port=port.ofport,
dl_src=mac_addr,
nw_src=ip_addr,
actions='ct(table={:d},zone=NXM_NX_REG{:d}[0..15])'.format(
fwaas_ovs_consts.FW_RULES_EGRESS_TABLE,
fwaas_ovs_consts.REG_NET)
)
# Apply mac/ip pairs for IPv6
allowed_pairs = port.allowed_pairs_v6.union(
{(port.mac, ip_addr) for ip_addr in port.ipv6_addresses})
for mac_addr, ip_addr in allowed_pairs:
self._add_flow(
table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE,
priority=65,
reg_port=port.ofport,
in_port=port.ofport,
ct_state=fwaas_ovs_consts.OF_STATE_NOT_TRACKED,
dl_type=lib_const.ETHERTYPE_IPV6,
dl_src=mac_addr,
ipv6_src=ip_addr,
actions='ct(table={:d},zone=NXM_NX_REG{:d}[0..15])'.format(
fwaas_ovs_consts.FW_RULES_EGRESS_TABLE,
fwaas_ovs_consts.REG_NET)
)
# DHCP discovery
accept_or_ingress = fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE
if self.sg_with_ovs:
accept_or_ingress = ovs_consts.ACCEPT_OR_INGRESS_TABLE
for dl_type, src_port, dst_port in (
(lib_const.ETHERTYPE_IP, 68, 67),
(lib_const.ETHERTYPE_IPV6, 546, 547)):
self._add_flow(
table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE,
priority=80,
reg_port=port.ofport,
in_port=port.ofport,
dl_type=dl_type,
nw_proto=lib_const.PROTO_NUM_UDP,
tp_src=src_port,
tp_dst=dst_port,
actions='resubmit(,{:d})'.format(accept_or_ingress)
)
# Ban dhcp service running on an instance
for dl_type, src_port, dst_port in (
(lib_const.ETHERTYPE_IP, 67, 68),
(lib_const.ETHERTYPE_IPV6, 547, 546)):
self._add_flow(
table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE,
priority=70,
in_port=port.ofport,
reg_port=port.ofport,
dl_type=dl_type,
nw_proto=lib_const.PROTO_NUM_UDP,
tp_src=src_port,
tp_dst=dst_port,
actions='resubmit(,%d)' % ovs_consts.DROPPED_TRAFFIC_TABLE
)
# Drop Router Advertisements from instances
self._add_flow(
table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE,
priority=70,
in_port=port.ofport,
reg_port=port.ofport,
dl_type=lib_const.ETHERTYPE_IPV6,
nw_proto=lib_const.PROTO_NUM_IPV6_ICMP,
icmp_type=lib_const.ICMPV6_TYPE_RA,
actions='resubmit(,%d)' % ovs_consts.DROPPED_TRAFFIC_TABLE
)
# Drop all remaining not tracked egress connections
self._add_flow(
table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE,
priority=10,
ct_state=fwaas_ovs_consts.OF_STATE_NOT_TRACKED,
in_port=port.ofport,
reg_port=port.ofport,
actions='resubmit(,%d)' % ovs_consts.DROPPED_TRAFFIC_TABLE
)
# Fill in accept_or_ingress table by checking that traffic is ingress
# and if not, accept it
if self.sg_with_ovs:
self._fwaas_process_colocated_ingress(port)
else:
for mac_addr in port.all_allowed_macs:
self._add_flow(
table=fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE,
priority=100,
dl_dst=mac_addr,
reg_net=port.vlan_tag,
actions='set_field:{:d}->reg{:d},resubmit(,{:d})'.format(
port.ofport,
fwaas_ovs_consts.REG_PORT,
fwaas_ovs_consts.FW_BASE_INGRESS_TABLE),
)
for ethertype in [lib_const.ETHERTYPE_IP,
lib_const.ETHERTYPE_IPV6]:
self._add_flow(
table=fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE,
priority=90,
dl_type=ethertype,
reg_port=port.ofport,
ct_state=fwaas_ovs_consts.OF_STATE_NEW_NOT_ESTABLISHED,
actions='ct(commit,zone=NXM_NX_REG{:d}[0..15]),'
'resubmit(,{:d})'.format(
fwaas_ovs_consts.REG_NET,
ovs_consts.ACCEPTED_EGRESS_TRAFFIC_TABLE)
)
self._add_flow(
table=fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE,
priority=80,
reg_port=port.ofport,
actions='normal'
)
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
# which differs in constants (table numbers)
def _initialize_tracked_egress(self, port):
# Drop invalid packets
self._add_flow(
table=fwaas_ovs_consts.FW_RULES_EGRESS_TABLE,
priority=50,
ct_state=fwaas_ovs_consts.OF_STATE_INVALID,
actions='resubmit(,%d)' % ovs_consts.DROPPED_TRAFFIC_TABLE
)
# Drop traffic for removed fwg rules
self._add_flow(
table=fwaas_ovs_consts.FW_RULES_EGRESS_TABLE,
priority=50,
reg_port=port.ofport,
ct_mark=fwaas_ovs_consts.CT_MARK_INVALID,
actions='resubmit(,%d)' % ovs_consts.DROPPED_TRAFFIC_TABLE
)
for state in (
fwaas_ovs_consts.OF_STATE_ESTABLISHED_REPLY,
fwaas_ovs_consts.OF_STATE_RELATED,
):
self._add_flow(
table=fwaas_ovs_consts.FW_RULES_EGRESS_TABLE,
priority=50,
ct_state=state,
ct_mark=fwaas_ovs_consts.CT_MARK_NORMAL,
reg_port=port.ofport,
ct_zone=port.vlan_tag,
actions='normal'
)
self._add_flow(
table=fwaas_ovs_consts.FW_RULES_EGRESS_TABLE,
priority=40,
reg_port=port.ofport,
ct_state=fwaas_ovs_consts.OF_STATE_NOT_ESTABLISHED,
actions='resubmit(,%d)' % ovs_consts.DROPPED_TRAFFIC_TABLE
)
for ethertype in [lib_const.ETHERTYPE_IP, lib_const.ETHERTYPE_IPV6]:
self._add_flow(
table=fwaas_ovs_consts.FW_RULES_EGRESS_TABLE,
priority=40,
dl_type=ethertype,
reg_port=port.ofport,
ct_state=fwaas_ovs_consts.OF_STATE_ESTABLISHED,
actions="ct(commit,zone=NXM_NX_REG{:d}[0..15],"
"exec(set_field:{:s}->ct_mark))".format(
fwaas_ovs_consts.REG_NET,
fwaas_ovs_consts.CT_MARK_INVALID)
)
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
# which differs in constants (table numbers)
def _initialize_ingress_ipv6_icmp(self, port):
for icmp_type in firewall.ICMPV6_ALLOWED_INGRESS_TYPES:
self._add_flow(
table=fwaas_ovs_consts.FW_BASE_INGRESS_TABLE,
priority=100,
reg_port=port.ofport,
dl_dst=port.mac,
dl_type=lib_const.ETHERTYPE_IPV6,
nw_proto=lib_const.PROTO_NUM_IPV6_ICMP,
icmp_type=icmp_type,
actions='output:{:d}'.format(port.ofport)
)
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
# which differs in constants (table numbers)
def _initialize_ingress(self, port):
# Allow incoming ARPs
self._add_flow(
table=fwaas_ovs_consts.FW_BASE_INGRESS_TABLE,
priority=100,
dl_type=lib_const.ETHERTYPE_ARP,
reg_port=port.ofport,
actions='output:{:d}'.format(port.ofport)
)
self._initialize_ingress_ipv6_icmp(port)
# DHCP offers
for dl_type, src_port, dst_port in (
(lib_const.ETHERTYPE_IP, 67, 68),
(lib_const.ETHERTYPE_IPV6, 547, 546)):
self._add_flow(
table=fwaas_ovs_consts.FW_BASE_INGRESS_TABLE,
priority=95,
reg_port=port.ofport,
dl_type=dl_type,
nw_proto=lib_const.PROTO_NUM_UDP,
tp_src=src_port,
tp_dst=dst_port,
actions='output:{:d}'.format(port.ofport)
)
# Track untracked
for dl_type in (lib_const.ETHERTYPE_IP, lib_const.ETHERTYPE_IPV6):
self._add_flow(
table=fwaas_ovs_consts.FW_BASE_INGRESS_TABLE,
priority=90,
reg_port=port.ofport,
dl_type=dl_type,
ct_state=fwaas_ovs_consts.OF_STATE_NOT_TRACKED,
actions='ct(table={:d},zone=NXM_NX_REG{:d}[0..15])'.format(
fwaas_ovs_consts.FW_RULES_INGRESS_TABLE,
fwaas_ovs_consts.REG_NET)
)
self._add_flow(
table=fwaas_ovs_consts.FW_BASE_INGRESS_TABLE,
ct_state=fwaas_ovs_consts.OF_STATE_TRACKED,
priority=80,
reg_port=port.ofport,
actions='resubmit(,{:d})'.format(
fwaas_ovs_consts.FW_RULES_INGRESS_TABLE)
)
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
# which differs in constants (table numbers)
def _initialize_tracked_ingress(self, port):
# Drop invalid packets
self._add_flow(
table=fwaas_ovs_consts.FW_RULES_INGRESS_TABLE,
priority=50,
ct_state=fwaas_ovs_consts.OF_STATE_INVALID,
actions='resubmit(,%d)' % ovs_consts.DROPPED_TRAFFIC_TABLE
)
# Drop traffic for removed fwg rules
self._add_flow(
table=fwaas_ovs_consts.FW_RULES_INGRESS_TABLE,
priority=50,
reg_port=port.ofport,
ct_mark=fwaas_ovs_consts.CT_MARK_INVALID,
actions='resubmit(,%d)' % ovs_consts.DROPPED_TRAFFIC_TABLE
)
# Allow established and related connections
for state in (fwaas_ovs_consts.OF_STATE_ESTABLISHED_REPLY,
fwaas_ovs_consts.OF_STATE_RELATED):
self._add_flow(
table=fwaas_ovs_consts.FW_RULES_INGRESS_TABLE,
priority=50,
reg_port=port.ofport,
ct_state=state,
ct_mark=fwaas_ovs_consts.CT_MARK_NORMAL,
ct_zone=port.vlan_tag,
actions='output:{:d}'.format(port.ofport)
)
self._add_flow(
table=fwaas_ovs_consts.FW_RULES_INGRESS_TABLE,
priority=40,
reg_port=port.ofport,
ct_state=fwaas_ovs_consts.OF_STATE_NOT_ESTABLISHED,
actions='resubmit(,%d)' % ovs_consts.DROPPED_TRAFFIC_TABLE
)
for ethertype in [lib_const.ETHERTYPE_IP, lib_const.ETHERTYPE_IPV6]:
self._add_flow(
table=fwaas_ovs_consts.FW_RULES_INGRESS_TABLE,
priority=40,
dl_type=ethertype,
reg_port=port.ofport,
ct_state=fwaas_ovs_consts.OF_STATE_ESTABLISHED,
actions="ct(commit,zone=NXM_NX_REG{:d}[0..15],"
"exec(set_field:{:s}->ct_mark))".format(
fwaas_ovs_consts.REG_NET,
fwaas_ovs_consts.CT_MARK_INVALID)
)
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
# which differs in constants (table numbers) and rules_generator method
def add_flows_from_rules(self, port):
self._initialize_tracked_ingress(port)
self._initialize_tracked_egress(port)
LOG.debug('Creating flow rules for port %s that is port %d in OVS',
port.id, port.ofport)
for rule in self.create_rules_generator_for_port(port):
flows = rules.create_flows_from_rule_and_port(rule, port)
LOG.debug("RULGEN: Rules generated for flow %s are %s",
rule, flows)
for flow in flows:
if rule.get('action') == ACTION_ALLOW:
self._accept_flow(**flow)
else:
self._drop_flow(**flow)
def create_rules_generator_for_port(self, port):
"""Returns a generator emitting rules valid for further processing
Injects necessary fields to feed one-by-one to rules module to
transform into valid openflow rules.
"""
def inject_fields(rule, direction, offset=0):
"""Add fields to rule dict to be able to utilize rules module
Currently such fields are added:
'offset', 'direction', 'ethertype', 'source_port_range_min',
'source_port_range_max', 'port_range_min', 'port_range_max'
"""
# XXX NOTE(ivasilevskaya) maybe there's a clever way to do that
version_ethertype_map = {lib_const.IP_VERSION_4: lib_const.IPv4,
lib_const.IP_VERSION_6: lib_const.IPv6}
rule['direction'] = direction
rule['ethertype'] = version_ethertype_map[rule['ip_version']]
rule['offset'] = offset
# transfer destination_port into port_range_min/port_range_max
def add_range(range_key, key_min, key_max):
range_str = rule.get(range_key)
if not range_str:
return
ports = range_str.split(':', 1)
rule[key_min] = int(ports[0])
rule['port_range_max'] = (
int(ports[1]) if len(ports) == 2 else int(ports[0]))
add_range('destination_port', 'port_range_min', 'port_range_max')
add_range('source_port', 'source_port_range_min',
'source_port_range_max')
# add direction field
offset = len(port.fw_group.ingress_rules) - 1
for rule in port.fw_group.ingress_rules:
inject_fields(rule, lib_const.INGRESS_DIRECTION, offset)
offset -= 1
yield rule
offset = len(port.fw_group.egress_rules) - 1
for rule in port.fw_group.egress_rules:
inject_fields(rule, lib_const.EGRESS_DIRECTION, offset)
offset -= 1
yield rule
# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
# which differs in constants (table numbers)
def delete_all_port_flows(self, port):
"""Delete all flows for given port"""
accept_or_ingress = fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE
if self.sg_with_ovs:
accept_or_ingress = ovs_consts.ACCEPT_OR_INGRESS_TABLE
for mac_addr in port.all_allowed_macs:
self._strict_delete_flow(priority=95,
table=ovs_consts.TRANSIENT_TABLE,
dl_dst=mac_addr,
dl_vlan=port.vlan_tag)
self._delete_flows(
table=accept_or_ingress,
dl_dst=mac_addr, reg_net=port.vlan_tag)
self._strict_delete_flow(priority=105,
table=ovs_consts.TRANSIENT_TABLE,
in_port=port.ofport)
self._delete_flows(reg_port=port.ofport)
def create_firewall_group(self, ports_for_fwg, firewall_group):
egress_rules = firewall_group['egress_rule_list']
ingress_rules = firewall_group['ingress_rule_list']
fwg_id = firewall_group['id']
self.update_firewall_group_rules(fwg_id, ingress_rules, egress_rules)
for port in ports_for_fwg:
port['firewall_group'] = fwg_id
self.update_port_filter(port)
def update_firewall_group(self, ports_for_fwg, firewall_group):
self.create_firewall_group(ports_for_fwg, firewall_group)
def delete_firewall_group(self, ports_for_fwg, firewall_group):
for port in ports_for_fwg:
port['firewall_group'] = firewall_group['id']
self.remove_port_filter(port)