371 lines
13 KiB
Python
371 lines
13 KiB
Python
# Copyright 2015 Red Hat, Inc.
|
|
# 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 collections
|
|
|
|
import netaddr
|
|
from neutron_lib import constants as n_consts
|
|
|
|
from neutron.agent.linux.openvswitch_firewall import constants as ovsfw_consts
|
|
from neutron.common import utils
|
|
from neutron.plugins.ml2.drivers.openvswitch.agent.common import constants \
|
|
as ovs_consts
|
|
|
|
CT_STATES = [
|
|
ovsfw_consts.OF_STATE_ESTABLISHED_NOT_REPLY,
|
|
ovsfw_consts.OF_STATE_NEW_NOT_ESTABLISHED]
|
|
|
|
FLOW_FIELD_FOR_IPVER_AND_DIRECTION = {
|
|
(n_consts.IP_VERSION_4, n_consts.EGRESS_DIRECTION): 'nw_dst',
|
|
(n_consts.IP_VERSION_6, n_consts.EGRESS_DIRECTION): 'ipv6_dst',
|
|
(n_consts.IP_VERSION_4, n_consts.INGRESS_DIRECTION): 'nw_src',
|
|
(n_consts.IP_VERSION_6, n_consts.INGRESS_DIRECTION): 'ipv6_src',
|
|
}
|
|
|
|
FORBIDDEN_PREFIXES = (n_consts.IPv4_ANY, n_consts.IPv6_ANY)
|
|
|
|
|
|
def is_valid_prefix(ip_prefix):
|
|
# IPv6 have multiple ways how to describe ::/0 network, converting to
|
|
# IPNetwork and back to string unifies it
|
|
return (ip_prefix and
|
|
str(netaddr.IPNetwork(ip_prefix)) not in FORBIDDEN_PREFIXES)
|
|
|
|
|
|
def _assert_mergeable_rules(rule_conj_list):
|
|
"""Assert a given (rule, conj_ids) list has mergeable rules.
|
|
|
|
The given rules must be the same except for port_range_{min,max}
|
|
differences.
|
|
"""
|
|
rule_tmpl = rule_conj_list[0][0].copy()
|
|
rule_tmpl.pop('port_range_min', None)
|
|
rule_tmpl.pop('port_range_max', None)
|
|
for rule, conj_id in rule_conj_list[1:]:
|
|
rule1 = rule.copy()
|
|
rule1.pop('port_range_min', None)
|
|
rule1.pop('port_range_max', None)
|
|
if rule_tmpl != rule1:
|
|
raise RuntimeError(
|
|
"Incompatible SG rules detected: %(rule1)s and %(rule2)s. "
|
|
"They cannot be merged. This should not happen." %
|
|
{'rule1': rule_tmpl, 'rule2': rule})
|
|
|
|
|
|
def merge_common_rules(rule_conj_list):
|
|
"""Take a list of (rule, conj_id) and merge elements with the same rules.
|
|
Return a list of (rule, conj_id_list).
|
|
"""
|
|
if len(rule_conj_list) == 1:
|
|
rule, conj_id = rule_conj_list[0]
|
|
return [(rule, [conj_id])]
|
|
|
|
_assert_mergeable_rules(rule_conj_list)
|
|
rule_conj_map = collections.defaultdict(list)
|
|
for rule, conj_id in rule_conj_list:
|
|
rule_conj_map[(rule.get('port_range_min'),
|
|
rule.get('port_range_max'))].append(conj_id)
|
|
|
|
result = []
|
|
rule_tmpl = rule_conj_list[0][0]
|
|
rule_tmpl.pop('port_range_min', None)
|
|
rule_tmpl.pop('port_range_max', None)
|
|
for (port_min, port_max), conj_ids in rule_conj_map.items():
|
|
rule = rule_tmpl.copy()
|
|
if port_min is not None:
|
|
rule['port_range_min'] = port_min
|
|
if port_max is not None:
|
|
rule['port_range_max'] = port_max
|
|
result.append((rule, conj_ids))
|
|
return result
|
|
|
|
|
|
def _merge_port_ranges_helper(port_range_item):
|
|
# Sort with 'port' but 'min' things must come first.
|
|
port, m, dummy = port_range_item
|
|
return port * 2 + (0 if m == 'min' else 1)
|
|
|
|
|
|
def merge_port_ranges(rule_conj_list):
|
|
"""Take a list of (rule, conj_id) and transform into a list
|
|
whose rules don't overlap. Return a list of (rule, conj_id_list).
|
|
"""
|
|
if len(rule_conj_list) == 1:
|
|
rule, conj_id = rule_conj_list[0]
|
|
return [(rule, [conj_id])]
|
|
|
|
_assert_mergeable_rules(rule_conj_list)
|
|
port_ranges = []
|
|
for rule, conj_id in rule_conj_list:
|
|
port_ranges.append((rule.get('port_range_min', 1), 'min', conj_id))
|
|
port_ranges.append((rule.get('port_range_max', 65535), 'max', conj_id))
|
|
|
|
port_ranges.sort(key=_merge_port_ranges_helper)
|
|
|
|
# The idea here is to scan the port_ranges list in an ascending order,
|
|
# keeping active conjunction IDs and range in cur_conj and cur_range_min.
|
|
# A 'min' port_ranges item means an addition to cur_conj, while a 'max'
|
|
# item means a removal.
|
|
result = []
|
|
rule_tmpl = rule_conj_list[0][0]
|
|
cur_conj = set()
|
|
cur_range_min = None
|
|
for port, m, conj_id in port_ranges:
|
|
if m == 'min':
|
|
if cur_conj and cur_range_min != port:
|
|
rule = rule_tmpl.copy()
|
|
rule['port_range_min'] = cur_range_min
|
|
rule['port_range_max'] = port - 1
|
|
result.append((rule, list(cur_conj)))
|
|
cur_range_min = port
|
|
cur_conj.add(conj_id)
|
|
else:
|
|
if cur_range_min <= port:
|
|
rule = rule_tmpl.copy()
|
|
rule['port_range_min'] = cur_range_min
|
|
rule['port_range_max'] = port
|
|
result.append((rule, list(cur_conj)))
|
|
# The next port range without 'port' starts from (port + 1)
|
|
cur_range_min = port + 1
|
|
cur_conj.remove(conj_id)
|
|
|
|
if (len(result) == 1 and result[0][0]['port_range_min'] == 1 and
|
|
result[0][0]['port_range_max'] == 65535):
|
|
del result[0][0]['port_range_min']
|
|
del result[0][0]['port_range_max']
|
|
return result
|
|
|
|
|
|
def flow_priority_offset(rule, conjunction=False):
|
|
"""Calculate flow priority offset from rule.
|
|
Whether the rule belongs to conjunction flows or not is decided
|
|
upon existence of rule['remote_group_id'] but can be overridden
|
|
to be True using the optional conjunction arg.
|
|
"""
|
|
conj_offset = 0 if 'remote_group_id' in rule or conjunction else 4
|
|
protocol = rule.get('protocol')
|
|
if protocol is None:
|
|
return conj_offset
|
|
|
|
if protocol in [n_consts.PROTO_NUM_ICMP, n_consts.PROTO_NUM_IPV6_ICMP]:
|
|
if 'port_range_min' not in rule:
|
|
return conj_offset + 1
|
|
elif 'port_range_max' not in rule:
|
|
return conj_offset + 2
|
|
return conj_offset + 3
|
|
|
|
|
|
def create_flows_from_rule_and_port(rule, port, conjunction=False):
|
|
"""Create flows from given args.
|
|
For description of the optional conjunction arg, see flow_priority_offset.
|
|
"""
|
|
ethertype = rule['ethertype']
|
|
direction = rule['direction']
|
|
dst_ip_prefix = rule.get('dest_ip_prefix')
|
|
src_ip_prefix = rule.get('source_ip_prefix')
|
|
|
|
flow_template = {
|
|
'priority': 70 + flow_priority_offset(rule, conjunction),
|
|
'dl_type': ovsfw_consts.ethertype_to_dl_type_map[ethertype],
|
|
'reg_port': port.ofport,
|
|
}
|
|
|
|
if is_valid_prefix(dst_ip_prefix):
|
|
flow_template[FLOW_FIELD_FOR_IPVER_AND_DIRECTION[(
|
|
utils.get_ip_version(dst_ip_prefix), n_consts.EGRESS_DIRECTION)]
|
|
] = dst_ip_prefix
|
|
|
|
if is_valid_prefix(src_ip_prefix):
|
|
flow_template[FLOW_FIELD_FOR_IPVER_AND_DIRECTION[(
|
|
utils.get_ip_version(src_ip_prefix), n_consts.INGRESS_DIRECTION)]
|
|
] = src_ip_prefix
|
|
|
|
flows = create_protocol_flows(direction, flow_template, port, rule)
|
|
|
|
return flows
|
|
|
|
|
|
def populate_flow_common(direction, flow_template, port):
|
|
"""Initialize common flow fields."""
|
|
if direction == n_consts.INGRESS_DIRECTION:
|
|
flow_template['table'] = ovs_consts.RULES_INGRESS_TABLE
|
|
flow_template['actions'] = "output:{:d},resubmit(,{:d})".format(
|
|
port.ofport,
|
|
ovs_consts.ACCEPTED_INGRESS_TRAFFIC_TABLE)
|
|
elif direction == n_consts.EGRESS_DIRECTION:
|
|
flow_template['table'] = ovs_consts.RULES_EGRESS_TABLE
|
|
# Traffic can be both ingress and egress, check that no ingress rules
|
|
# should be applied
|
|
flow_template['actions'] = 'resubmit(,{:d})'.format(
|
|
ovs_consts.ACCEPT_OR_INGRESS_TABLE)
|
|
return flow_template
|
|
|
|
|
|
def create_protocol_flows(direction, flow_template, port, rule):
|
|
flow_template = populate_flow_common(direction,
|
|
flow_template.copy(),
|
|
port)
|
|
protocol = rule.get('protocol')
|
|
if protocol is not None:
|
|
flow_template['nw_proto'] = protocol
|
|
|
|
if protocol in [n_consts.PROTO_NUM_ICMP, n_consts.PROTO_NUM_IPV6_ICMP]:
|
|
flows = create_icmp_flows(flow_template, rule)
|
|
else:
|
|
flows = create_port_range_flows(flow_template, rule)
|
|
return flows or [flow_template]
|
|
|
|
|
|
def create_port_range_flows(flow_template, rule):
|
|
protocol = ovsfw_consts.REVERSE_IP_PROTOCOL_MAP_WITH_PORTS.get(
|
|
rule.get('protocol'))
|
|
if protocol is None:
|
|
return []
|
|
flows = []
|
|
src_port_match = '{:s}_src'.format(protocol)
|
|
src_port_min = rule.get('source_port_range_min')
|
|
src_port_max = rule.get('source_port_range_max')
|
|
dst_port_match = '{:s}_dst'.format(protocol)
|
|
dst_port_min = rule.get('port_range_min')
|
|
dst_port_max = rule.get('port_range_max')
|
|
|
|
dst_port_range = []
|
|
if dst_port_min and dst_port_max:
|
|
dst_port_range = utils.port_rule_masking(dst_port_min, dst_port_max)
|
|
|
|
src_port_range = []
|
|
if src_port_min and src_port_max:
|
|
src_port_range = utils.port_rule_masking(src_port_min, src_port_max)
|
|
for port in src_port_range:
|
|
flow = flow_template.copy()
|
|
flow[src_port_match] = port
|
|
if dst_port_range:
|
|
for port in dst_port_range:
|
|
dst_flow = flow.copy()
|
|
dst_flow[dst_port_match] = port
|
|
flows.append(dst_flow)
|
|
else:
|
|
flows.append(flow)
|
|
else:
|
|
for port in dst_port_range:
|
|
flow = flow_template.copy()
|
|
flow[dst_port_match] = port
|
|
flows.append(flow)
|
|
|
|
return flows
|
|
|
|
|
|
def create_icmp_flows(flow_template, rule):
|
|
icmp_type = rule.get('port_range_min')
|
|
if icmp_type is None:
|
|
return
|
|
flow = flow_template.copy()
|
|
flow['icmp_type'] = icmp_type
|
|
|
|
icmp_code = rule.get('port_range_max')
|
|
if icmp_code is not None:
|
|
flow['icmp_code'] = icmp_code
|
|
return [flow]
|
|
|
|
|
|
def _flow_priority_offset_from_conj_id(conj_id):
|
|
"Return a flow priority offset encoded in a conj_id."
|
|
# A base conj_id, which is returned by ConjIdMap.get_conj_id, is a
|
|
# multiple of 8, and we use 2 conj_ids per offset.
|
|
return conj_id % 8 // 2
|
|
|
|
|
|
def create_flows_for_ip_address(ip_address, direction, ethertype,
|
|
vlan_tag, conj_ids):
|
|
"""Create flows from a rule and an ip_address derived from
|
|
remote_group_id
|
|
"""
|
|
|
|
# Group conj_ids per priority.
|
|
conj_id_lists = [[] for i in range(4)]
|
|
for conj_id in conj_ids:
|
|
conj_id_lists[
|
|
_flow_priority_offset_from_conj_id(conj_id)].append(conj_id)
|
|
|
|
ip_prefix = str(netaddr.IPNetwork(ip_address).cidr)
|
|
|
|
flow_template = {
|
|
'dl_type': ovsfw_consts.ethertype_to_dl_type_map[ethertype],
|
|
'reg_net': vlan_tag, # needed for project separation
|
|
}
|
|
|
|
ip_ver = utils.get_ip_version(ip_prefix)
|
|
|
|
if direction == n_consts.EGRESS_DIRECTION:
|
|
flow_template['table'] = ovs_consts.RULES_EGRESS_TABLE
|
|
elif direction == n_consts.INGRESS_DIRECTION:
|
|
flow_template['table'] = ovs_consts.RULES_INGRESS_TABLE
|
|
|
|
flow_template[FLOW_FIELD_FOR_IPVER_AND_DIRECTION[(
|
|
ip_ver, direction)]] = ip_prefix
|
|
|
|
result = []
|
|
for offset, conj_id_list in enumerate(conj_id_lists):
|
|
if not conj_id_list:
|
|
continue
|
|
flow_template['priority'] = 70 + offset
|
|
result.extend(
|
|
substitute_conjunction_actions([flow_template], 1, conj_id_list))
|
|
return result
|
|
|
|
|
|
def create_accept_flows(flow):
|
|
flow['ct_state'] = CT_STATES[0]
|
|
result = [flow.copy()]
|
|
flow['ct_state'] = CT_STATES[1]
|
|
if flow['table'] == ovs_consts.RULES_INGRESS_TABLE:
|
|
flow['actions'] = (
|
|
'ct(commit,zone=NXM_NX_REG{:d}[0..15]),{:s}'.format(
|
|
ovsfw_consts.REG_NET, flow['actions']))
|
|
result.append(flow)
|
|
return result
|
|
|
|
|
|
def substitute_conjunction_actions(flows, dimension, conj_ids):
|
|
result = []
|
|
for flow in flows:
|
|
for i in range(2):
|
|
new_flow = flow.copy()
|
|
new_flow['ct_state'] = CT_STATES[i]
|
|
new_flow['actions'] = ','.join(
|
|
["conjunction(%d,%d/2)" % (s + i, dimension)
|
|
for s in conj_ids])
|
|
result.append(new_flow)
|
|
|
|
return result
|
|
|
|
|
|
def create_conj_flows(port, conj_id, direction, ethertype):
|
|
"""Generate "accept" flows for a given conjunction ID."""
|
|
flow_template = {
|
|
'priority': 70 + _flow_priority_offset_from_conj_id(conj_id),
|
|
'conj_id': conj_id,
|
|
'dl_type': ovsfw_consts.ethertype_to_dl_type_map[ethertype],
|
|
# This reg_port matching is for delete_all_port_flows.
|
|
# The matching is redundant as it has been done by
|
|
# conjunction(...,2/2) flows and flows can be summarized
|
|
# without this.
|
|
'reg_port': port.ofport,
|
|
}
|
|
flow_template = populate_flow_common(direction, flow_template, port)
|
|
flows = create_accept_flows(flow_template)
|
|
flows[1]['conj_id'] += 1
|
|
return flows
|