Better handle ports in security groups

After taking a closer look at bug 1818385, I found a couple
of follow-on things to fix in the security group code.

First, there are very few protocols that accept ports,
especially via iptables.  For this reason I think it's
acceptable that the API rejects them as invalid.

Second, UDPlite has some interesting support in iptables.  It
does not support using --dport directly, but does using
'-m multiport --dports 123', and also supports port ranges using
'-m multiport --dports 123:124'.  Added code for this special
case.

Change-Id: Ifb2e6bb6c7a2e2987ba95040ef5a98ed50aa36d4
Closes-Bug: #1818385
This commit is contained in:
Brian Haley 2019-03-08 15:24:24 -05:00
parent 1ef77b1796
commit 4350ed3c35
8 changed files with 102 additions and 34 deletions

View File

@ -46,15 +46,6 @@ IPSET_DIRECTION = {constants.INGRESS_DIRECTION: 'src',
comment_rule = iptables_manager.comment_rule
libc = ctypes.CDLL(util.find_library('libc.so.6'))
# iptables protocols that support --dport and --sport
IPTABLES_PORT_PROTOCOLS = [
constants.PROTO_NAME_DCCP,
constants.PROTO_NAME_SCTP,
constants.PROTO_NAME_TCP,
constants.PROTO_NAME_UDP,
constants.PROTO_NAME_UDPLITE
]
def get_hybrid_port_name(port_name):
return (constants.TAP_DEVICE_PREFIX + port_name)[:n_const.LINUX_DEV_LEN]
@ -742,9 +733,15 @@ class IptablesFirewallDriver(firewall.FirewallDriver):
# icmp code can be 0 so we cannot use "if port_range_max" here
if port_range_max is not None:
args[-1] += '/%s' % port_range_max
elif protocol in IPTABLES_PORT_PROTOCOLS:
elif protocol in n_const.SG_PORT_PROTO_NAMES:
# iptables protocols that support --dport, --sport and -m multiport
if port_range_min == port_range_max:
args += ['--%s' % direction, '%s' % (port_range_min,)]
if protocol in n_const.IPTABLES_MULTIPORT_ONLY_PROTOCOLS:
# use -m multiport, but without a port range
args += ['-m', 'multiport', '--%ss' % direction,
'%s' % port_range_min]
else:
args += ['--%s' % direction, '%s' % port_range_min]
else:
args += ['-m', 'multiport', '--%ss' % direction,
'%s:%s' % (port_range_min, port_range_max)]

View File

@ -134,6 +134,28 @@ IPTABLES_PROTOCOL_NAME_MAP = {lib_constants.PROTO_NAME_IPV6_ENCAP: 'ipv6',
'141': 'wesp',
'142': 'rohc'}
# Security group protocols that support ports
SG_PORT_PROTO_NUMS = [
lib_constants.PROTO_NUM_DCCP,
lib_constants.PROTO_NUM_SCTP,
lib_constants.PROTO_NUM_TCP,
lib_constants.PROTO_NUM_UDP,
lib_constants.PROTO_NUM_UDPLITE
]
SG_PORT_PROTO_NAMES = [
lib_constants.PROTO_NAME_DCCP,
lib_constants.PROTO_NAME_SCTP,
lib_constants.PROTO_NAME_TCP,
lib_constants.PROTO_NAME_UDP,
lib_constants.PROTO_NAME_UDPLITE
]
# iptables protocols that only support --dport and --sport using -m multiport
IPTABLES_MULTIPORT_ONLY_PROTOCOLS = [
lib_constants.PROTO_NAME_UDPLITE
]
# A length of a iptables chain name must be less than or equal to 11
# characters.
# <max length of iptables chain name> - (<binary_name> + '-') = 28-(16+1) = 11

View File

@ -473,14 +473,14 @@ class SecurityGroupDbMixin(ext_sg.SecurityGroupPluginBase,
ip_proto = self._get_ip_proto_number(rule['protocol'])
# Not all firewall_driver support all these protocols,
# but being strict here doesn't hurt.
if ip_proto in [constants.PROTO_NUM_DCCP, constants.PROTO_NUM_SCTP,
constants.PROTO_NUM_TCP, constants.PROTO_NUM_UDP,
constants.PROTO_NUM_UDPLITE]:
if (ip_proto in n_const.SG_PORT_PROTO_NUMS or
ip_proto in n_const.SG_PORT_PROTO_NAMES):
if rule['port_range_min'] == 0 or rule['port_range_max'] == 0:
raise ext_sg.SecurityGroupInvalidPortValue(port=0)
elif (rule['port_range_min'] is not None and
rule['port_range_max'] is not None and
rule['port_range_min'] <= rule['port_range_max']):
# When min/max are the same it is just a single port
pass
else:
raise ext_sg.SecurityGroupInvalidPortRange()
@ -496,13 +496,13 @@ class SecurityGroupDbMixin(ext_sg.SecurityGroupPluginBase,
raise ext_sg.SecurityGroupMissingIcmpType(
value=rule['port_range_max'])
else:
# Only the protocols above support port ranges, raise otherwise.
# When min/max are the same it is just a single port.
if (rule['port_range_min'] is not None and
rule['port_range_max'] is not None and
rule['port_range_min'] != rule['port_range_max']):
raise ext_sg.SecurityGroupInvalidProtocolForPortRange(
protocol=ip_proto)
# Only the protocols above support ports, raise otherwise.
if (rule['port_range_min'] is not None or
rule['port_range_max'] is not None):
port_protocols = (
', '.join(s.upper() for s in n_const.SG_PORT_PROTO_NAMES))
raise ext_sg.SecurityGroupInvalidProtocolForPort(
protocol=ip_proto, valid_port_protocols=port_protocols)
def _validate_ethertype_and_protocol(self, rule):
"""Check if given ethertype and protocol are valid or not"""

View File

@ -40,10 +40,9 @@ class SecurityGroupInvalidPortRange(exceptions.InvalidInput):
"<= port_range_max")
class SecurityGroupInvalidProtocolForPortRange(exceptions.InvalidInput):
message = _("Port range cannot be specified for protocol %(protocol)s. "
"Port range is only supported for "
"TCP, UDP, UDPLITE, SCTP and DCCP.")
class SecurityGroupInvalidProtocolForPort(exceptions.InvalidInput):
message = _("Ports cannot be specified for protocol %(protocol)s. "
"Ports are only supported for %(valid_port_protocols)s.")
class SecurityGroupInvalidPortValue(exceptions.InvalidInput):

View File

@ -415,6 +415,32 @@ class IptablesFirewallTestCase(BaseIptablesFirewallTestCase):
egress = None
self._test_prepare_port_filter(rule, ingress, egress)
def test_filter_ipv4_ingress_udplite_port(self):
rule = {'ethertype': 'IPv4',
'direction': 'ingress',
'protocol': 'udplite',
'port_range_min': 10,
'port_range_max': 10}
ingress = mock.call.add_rule(
'ifake_dev',
'-p udplite -m multiport --dports 10 -j RETURN',
top=False, comment=None)
egress = None
self._test_prepare_port_filter(rule, ingress, egress)
def test_filter_ipv4_ingress_udplite_mport(self):
rule = {'ethertype': 'IPv4',
'direction': 'ingress',
'protocol': 'udplite',
'port_range_min': 10,
'port_range_max': 100}
ingress = mock.call.add_rule(
'ifake_dev',
'-p udplite -m multiport --dports 10:100 -j RETURN',
top=False, comment=None)
egress = None
self._test_prepare_port_filter(rule, ingress, egress)
def test_filter_ipv4_ingress_protocol_blank(self):
rule = {'ethertype': 'IPv4',
'direction': 'ingress',

View File

@ -460,8 +460,20 @@ class SecurityGroupDbMixinTestCase(testlib_api.SqlTestCase):
'port_range_max': 1,
'protocol': constants.PROTO_NAME_UDPLITE})
self.assertRaises(
securitygroup.SecurityGroupInvalidProtocolForPortRange,
securitygroup.SecurityGroupInvalidProtocolForPort,
self.mixin._validate_port_range,
{'port_range_min': 100,
'port_range_max': 200,
'protocol': '111'})
self.assertRaises(
securitygroup.SecurityGroupInvalidProtocolForPort,
self.mixin._validate_port_range,
{'port_range_min': 100,
'port_range_max': None,
'protocol': constants.PROTO_NAME_VRRP})
self.assertRaises(
securitygroup.SecurityGroupInvalidProtocolForPort,
self.mixin._validate_port_range,
{'port_range_min': None,
'port_range_max': 200,
'protocol': constants.PROTO_NAME_VRRP})

View File

@ -608,17 +608,18 @@ class TestSecurityGroups(SecurityGroupDBTestCase):
self.deserialize(self.fmt, res)
self.assertEqual(webob.exc.HTTPCreated.code, res.status_int)
def test_create_security_group_rule_protocol_as_number_with_port(self):
def test_create_security_group_rule_protocol_as_number_with_port_bad(self):
# When specifying ports, neither can be None
name = 'webservers'
description = 'my webservers'
with self.security_group(name, description) as sg:
security_group_id = sg['security_group']['id']
protocol = 111
protocol = 6
rule = self._build_security_group_rule(
security_group_id, 'ingress', protocol, '70')
security_group_id, 'ingress', protocol, '70', None)
res = self._create_security_group_rule(self.fmt, rule)
self.deserialize(self.fmt, res)
self.assertEqual(webob.exc.HTTPCreated.code, res.status_int)
self.assertEqual(webob.exc.HTTPBadRequest.code, res.status_int)
def test_create_security_group_rule_protocol_as_number_range(self):
# This is a SG rule with a port range, but treated as a single
@ -627,22 +628,22 @@ class TestSecurityGroups(SecurityGroupDBTestCase):
description = 'my webservers'
with self.security_group(name, description) as sg:
security_group_id = sg['security_group']['id']
protocol = 111
protocol = 6
rule = self._build_security_group_rule(
security_group_id, 'ingress', protocol, '70', '70')
res = self._create_security_group_rule(self.fmt, rule)
self.deserialize(self.fmt, res)
self.assertEqual(webob.exc.HTTPCreated.code, res.status_int)
def test_create_security_group_rule_protocol_as_number_range_bad(self):
# Only certain protocols support a SG rule with a port range
def test_create_security_group_rule_protocol_as_number_port_bad(self):
# Only certain protocols support a SG rule with a port
name = 'webservers'
description = 'my webservers'
with self.security_group(name, description) as sg:
security_group_id = sg['security_group']['id']
protocol = 111
rule = self._build_security_group_rule(
security_group_id, 'ingress', protocol, '70', '71')
security_group_id, 'ingress', protocol, '70', '70')
res = self._create_security_group_rule(self.fmt, rule)
self.deserialize(self.fmt, res)
self.assertEqual(webob.exc.HTTPBadRequest.code, res.status_int)

View File

@ -0,0 +1,11 @@
---
upgrade:
- |
The Neutron API now enforces that ports are a valid option for
security group rules based on the protocol given, instead of
relying on the backend firewall driver to do this enforcement,
typically silently ignoring the port option in the rule. The
valid set of whitelisted protocols that support ports are TCP,
UDP, UDPLITE, SCTP and DCCP. Ports used with other protocols
will now generate an HTTP 400 error. For more information, see
bug `1818385 <https://bugs.launchpad.net/neutron/+bug/1818385>`_.