350 lines
12 KiB
Python
350 lines
12 KiB
Python
#
|
|
# 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 const
|
|
from neutron_lib import exceptions as n_exceptions
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
|
|
from neutron._i18n import _
|
|
from neutron.common.ovn import constants as ovn_const
|
|
from neutron.common.ovn import utils
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
# Convert the protocol number from integer to strings because that's
|
|
# how Neutron will pass it to us
|
|
PROTOCOL_NAME_TO_NUM_MAP = {k: str(v) for k, v in
|
|
const.IP_PROTOCOL_MAP.items()}
|
|
# Create a map from protocol numbers to names
|
|
PROTOCOL_NUM_TO_NAME_MAP = {v: k for k, v in
|
|
PROTOCOL_NAME_TO_NUM_MAP.items()}
|
|
|
|
# Group of transport protocols supported
|
|
TRANSPORT_PROTOCOLS = (const.PROTO_NAME_TCP,
|
|
const.PROTO_NAME_UDP,
|
|
const.PROTO_NAME_SCTP,
|
|
PROTOCOL_NAME_TO_NUM_MAP[const.PROTO_NAME_TCP],
|
|
PROTOCOL_NAME_TO_NUM_MAP[const.PROTO_NAME_UDP],
|
|
PROTOCOL_NAME_TO_NUM_MAP[const.PROTO_NAME_SCTP])
|
|
|
|
# Group of versions of the ICMP protocol supported
|
|
ICMP_PROTOCOLS = (const.PROTO_NAME_ICMP,
|
|
const.PROTO_NAME_IPV6_ICMP,
|
|
const.PROTO_NAME_IPV6_ICMP_LEGACY,
|
|
PROTOCOL_NAME_TO_NUM_MAP[const.PROTO_NAME_ICMP],
|
|
PROTOCOL_NAME_TO_NUM_MAP[const.PROTO_NAME_IPV6_ICMP],
|
|
PROTOCOL_NAME_TO_NUM_MAP[const.PROTO_NAME_IPV6_ICMP_LEGACY])
|
|
|
|
|
|
class ProtocolNotSupported(n_exceptions.NeutronException):
|
|
message = _('The protocol "%(protocol)s" is not supported. Valid '
|
|
'protocols are: %(valid_protocols)s; or protocol '
|
|
'numbers ranging from 0 to 255.')
|
|
|
|
|
|
def is_sg_enabled():
|
|
return cfg.CONF.SECURITYGROUP.enable_security_group
|
|
|
|
|
|
def acl_direction(r, port=None, port_group=None):
|
|
if r['direction'] == const.INGRESS_DIRECTION:
|
|
portdir = 'outport'
|
|
else:
|
|
portdir = 'inport'
|
|
|
|
if port:
|
|
return '%s == "%s"' % (portdir, port['id'])
|
|
return '%s == @%s' % (portdir, port_group)
|
|
|
|
|
|
def acl_ethertype(r):
|
|
match = ''
|
|
ip_version = None
|
|
icmp = None
|
|
if r['ethertype'] == const.IPv4:
|
|
match = ' && ip4'
|
|
ip_version = 'ip4'
|
|
icmp = 'icmp4'
|
|
elif r['ethertype'] == const.IPv6:
|
|
match = ' && ip6'
|
|
ip_version = 'ip6'
|
|
icmp = 'icmp6'
|
|
return match, ip_version, icmp
|
|
|
|
|
|
def acl_remote_ip_prefix(r, ip_version):
|
|
if not r['remote_ip_prefix']:
|
|
return ''
|
|
cidr = netaddr.IPNetwork(r['remote_ip_prefix'])
|
|
normalized_ip_prefix = "%s/%s" % (cidr.network, cidr.prefixlen)
|
|
if r['remote_ip_prefix'] != normalized_ip_prefix:
|
|
LOG.info("remote_ip_prefix %(remote_ip_prefix)s configured in "
|
|
"rule %(rule_id)s is not normalized. Normalized CIDR "
|
|
"%(normalized_ip_prefix)s will be used instead.",
|
|
{'remote_ip_prefix': r['remote_ip_prefix'],
|
|
'rule_id': r['id'],
|
|
'normalized_ip_prefix': normalized_ip_prefix})
|
|
src_or_dst = 'src' if r['direction'] == const.INGRESS_DIRECTION else 'dst'
|
|
return ' && %s.%s == %s' % (ip_version, src_or_dst, normalized_ip_prefix)
|
|
|
|
|
|
def _get_protocol_number(protocol):
|
|
if protocol is None:
|
|
return
|
|
try:
|
|
protocol = int(protocol)
|
|
if 0 <= protocol <= 255:
|
|
return str(protocol)
|
|
except (ValueError, TypeError):
|
|
protocol = PROTOCOL_NAME_TO_NUM_MAP.get(protocol)
|
|
if protocol is not None:
|
|
return protocol
|
|
|
|
raise ProtocolNotSupported(
|
|
protocol=protocol, valid_protocols=', '.join(PROTOCOL_NAME_TO_NUM_MAP))
|
|
|
|
|
|
def acl_protocol_and_ports(r, icmp):
|
|
match = ''
|
|
protocol = _get_protocol_number(r.get('protocol'))
|
|
if protocol is None:
|
|
return match
|
|
|
|
min_port = r.get('port_range_min')
|
|
max_port = r.get('port_range_max')
|
|
if protocol in TRANSPORT_PROTOCOLS:
|
|
protocol = PROTOCOL_NUM_TO_NAME_MAP[protocol]
|
|
match += ' && %s' % protocol
|
|
if min_port is not None and min_port == max_port:
|
|
match += ' && %s.dst == %d' % (protocol, min_port)
|
|
else:
|
|
if min_port is not None:
|
|
match += ' && %s.dst >= %d' % (protocol, min_port)
|
|
if max_port is not None:
|
|
match += ' && %s.dst <= %d' % (protocol, max_port)
|
|
elif protocol in ICMP_PROTOCOLS:
|
|
protocol = icmp
|
|
match += ' && %s' % protocol
|
|
if min_port is not None:
|
|
match += ' && %s.type == %d' % (protocol, min_port)
|
|
if max_port is not None:
|
|
match += ' && %s.code == %d' % (protocol, max_port)
|
|
else:
|
|
match += ' && ip.proto == %s' % protocol
|
|
|
|
return match
|
|
|
|
|
|
def add_acls_for_drop_port_group(pg_name):
|
|
acl_list = []
|
|
for direction, p in (('from-lport', 'inport'),
|
|
('to-lport', 'outport')):
|
|
acl = {"port_group": pg_name,
|
|
"priority": ovn_const.ACL_PRIORITY_DROP,
|
|
"action": ovn_const.ACL_ACTION_DROP,
|
|
"log": False,
|
|
"name": [],
|
|
"severity": [],
|
|
"direction": direction,
|
|
"match": '%s == @%s && ip' % (p, pg_name)}
|
|
acl_list.append(acl)
|
|
return acl_list
|
|
|
|
|
|
def drop_all_ip_traffic_for_port(port):
|
|
acl_list = []
|
|
for direction, p in (('from-lport', 'inport'),
|
|
('to-lport', 'outport')):
|
|
lswitch = utils.ovn_name(port['network_id'])
|
|
lport = port['id']
|
|
acl = {"lswitch": lswitch, "lport": lport,
|
|
"priority": ovn_const.ACL_PRIORITY_DROP,
|
|
"action": ovn_const.ACL_ACTION_DROP,
|
|
"log": False,
|
|
"name": [],
|
|
"severity": [],
|
|
"direction": direction,
|
|
"match": '%s == "%s" && ip' % (p, port['id']),
|
|
"external_ids": {'neutron:lport': port['id']}}
|
|
acl_list.append(acl)
|
|
return acl_list
|
|
|
|
|
|
def add_sg_rule_acl_for_port_group(port_group, r, match):
|
|
dir_map = {const.INGRESS_DIRECTION: 'to-lport',
|
|
const.EGRESS_DIRECTION: 'from-lport'}
|
|
acl = {"port_group": port_group,
|
|
"priority": ovn_const.ACL_PRIORITY_ALLOW,
|
|
"action": ovn_const.ACL_ACTION_ALLOW_RELATED,
|
|
"log": False,
|
|
"name": [],
|
|
"severity": [],
|
|
"direction": dir_map[r['direction']],
|
|
"match": match,
|
|
ovn_const.OVN_SG_RULE_EXT_ID_KEY: r['id']}
|
|
return acl
|
|
|
|
|
|
def add_acl_dhcp(port, subnet, ovn_dhcp=True):
|
|
# Allow DHCP requests for OVN native DHCP service, while responses are
|
|
# allowed in ovn-northd.
|
|
# Allow both DHCP requests and responses to pass for other DHCP services.
|
|
# We do this even if DHCP isn't enabled for the subnet
|
|
acl_list = []
|
|
if not ovn_dhcp:
|
|
acl = {"lswitch": utils.ovn_name(port['network_id']),
|
|
"lport": port['id'],
|
|
"priority": ovn_const.ACL_PRIORITY_ALLOW,
|
|
"action": ovn_const.ACL_ACTION_ALLOW,
|
|
"log": False,
|
|
"name": [],
|
|
"severity": [],
|
|
"direction": 'to-lport',
|
|
"match": ('outport == "%s" && ip4 && ip4.src == %s && '
|
|
'udp && udp.src == 67 && udp.dst == 68'
|
|
) % (port['id'], subnet['cidr']),
|
|
"external_ids": {'neutron:lport': port['id']}}
|
|
acl_list.append(acl)
|
|
acl = {"lswitch": utils.ovn_name(port['network_id']),
|
|
"lport": port['id'],
|
|
"priority": ovn_const.ACL_PRIORITY_ALLOW,
|
|
"action": ovn_const.ACL_ACTION_ALLOW,
|
|
"log": False,
|
|
"name": [],
|
|
"severity": [],
|
|
"direction": 'from-lport',
|
|
"match": ('inport == "%s" && ip4 && '
|
|
'ip4.dst == {255.255.255.255, %s} && '
|
|
'udp && udp.src == 68 && udp.dst == 67'
|
|
) % (port['id'], subnet['cidr']),
|
|
"external_ids": {'neutron:lport': port['id']}}
|
|
acl_list.append(acl)
|
|
return acl_list
|
|
|
|
|
|
def _get_subnet_from_cache(plugin, admin_context, subnet_cache, subnet_id):
|
|
if subnet_id in subnet_cache:
|
|
return subnet_cache[subnet_id]
|
|
else:
|
|
subnet = plugin.get_subnet(admin_context, subnet_id)
|
|
if subnet:
|
|
subnet_cache[subnet_id] = subnet
|
|
return subnet
|
|
|
|
|
|
def _get_sg_ports_from_cache(plugin, admin_context, sg_ports_cache, sg_id):
|
|
if sg_id in sg_ports_cache:
|
|
return sg_ports_cache[sg_id]
|
|
else:
|
|
filters = {'security_group_id': [sg_id]}
|
|
sg_ports = plugin._get_port_security_group_bindings(
|
|
admin_context, filters)
|
|
if sg_ports:
|
|
sg_ports_cache[sg_id] = sg_ports
|
|
return sg_ports
|
|
|
|
|
|
def _get_sg_from_cache(plugin, admin_context, sg_cache, sg_id):
|
|
if sg_id in sg_cache:
|
|
return sg_cache[sg_id]
|
|
else:
|
|
sg = plugin.get_security_group(admin_context, sg_id)
|
|
if sg:
|
|
sg_cache[sg_id] = sg
|
|
return sg
|
|
|
|
|
|
def acl_remote_group_id(r, ip_version):
|
|
if not r['remote_group_id']:
|
|
return ''
|
|
|
|
src_or_dst = 'src' if r['direction'] == const.INGRESS_DIRECTION else 'dst'
|
|
addrset_name = utils.ovn_pg_addrset_name(r['remote_group_id'],
|
|
ip_version)
|
|
return ' && %s.%s == $%s' % (ip_version, src_or_dst, addrset_name)
|
|
|
|
|
|
def _add_sg_rule_acl_for_port_group(port_group, r):
|
|
# Update the match based on which direction this rule is for (ingress
|
|
# or egress).
|
|
match = acl_direction(r, port_group=port_group)
|
|
|
|
# Update the match for IPv4 vs IPv6.
|
|
ip_match, ip_version, icmp = acl_ethertype(r)
|
|
match += ip_match
|
|
|
|
# Update the match if an IPv4 or IPv6 prefix was specified.
|
|
match += acl_remote_ip_prefix(r, ip_version)
|
|
|
|
# Update the match if remote group id was specified.
|
|
match += acl_remote_group_id(r, ip_version)
|
|
|
|
# Update the match for the protocol (tcp, udp, icmp) and port/type
|
|
# range if specified.
|
|
match += acl_protocol_and_ports(r, icmp)
|
|
|
|
# Finally, create the ACL entry for the direction specified.
|
|
return add_sg_rule_acl_for_port_group(port_group, r, match)
|
|
|
|
|
|
def _acl_columns_name_severity_supported(nb_idl):
|
|
columns = list(nb_idl._tables['ACL'].columns)
|
|
return ('name' in columns) and ('severity' in columns)
|
|
|
|
|
|
def add_acls_for_sg_port_group(ovn, security_group, txn):
|
|
for r in security_group['security_group_rules']:
|
|
acl = _add_sg_rule_acl_for_port_group(
|
|
utils.ovn_port_group_name(security_group['id']), r)
|
|
txn.add(ovn.pg_acl_add(**acl, may_exist=True))
|
|
|
|
|
|
def update_acls_for_security_group(plugin,
|
|
admin_context,
|
|
ovn,
|
|
security_group_id,
|
|
security_group_rule,
|
|
is_add_acl=True):
|
|
|
|
# Skip ACLs if security groups aren't enabled
|
|
if not is_sg_enabled():
|
|
return
|
|
|
|
# Check if ACL log name and severity supported or not
|
|
keep_name_severity = _acl_columns_name_severity_supported(ovn)
|
|
|
|
acl = _add_sg_rule_acl_for_port_group(
|
|
utils.ovn_port_group_name(security_group_id),
|
|
security_group_rule)
|
|
# Remove ACL log name and severity if not supported
|
|
if is_add_acl:
|
|
if not keep_name_severity:
|
|
acl.pop('name')
|
|
acl.pop('severity')
|
|
ovn.pg_acl_add(**acl, may_exist=True).execute(check_error=True)
|
|
else:
|
|
ovn.pg_acl_del(acl['port_group'], acl['direction'],
|
|
acl['priority'], acl['match']).execute(
|
|
check_error=True)
|
|
|
|
|
|
def filter_acl_dict(acl, extra_fields=None):
|
|
if extra_fields is None:
|
|
extra_fields = []
|
|
extra_fields.extend(ovn_const.ACL_EXPECTED_COLUMNS_NBDB)
|
|
return {k: acl[k] for k in extra_fields}
|