Browse Source

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
Brian Haley 1 month ago
parent
commit
4350ed3c35

+ 8
- 11
neutron/agent/linux/iptables_firewall.py View File

@@ -46,15 +46,6 @@ IPSET_DIRECTION = {constants.INGRESS_DIRECTION: 'src',
46 46
 comment_rule = iptables_manager.comment_rule
47 47
 libc = ctypes.CDLL(util.find_library('libc.so.6'))
48 48
 
49
-# iptables protocols that support --dport and --sport
50
-IPTABLES_PORT_PROTOCOLS = [
51
-    constants.PROTO_NAME_DCCP,
52
-    constants.PROTO_NAME_SCTP,
53
-    constants.PROTO_NAME_TCP,
54
-    constants.PROTO_NAME_UDP,
55
-    constants.PROTO_NAME_UDPLITE
56
-]
57
-
58 49
 
59 50
 def get_hybrid_port_name(port_name):
60 51
     return (constants.TAP_DEVICE_PREFIX + port_name)[:n_const.LINUX_DEV_LEN]
@@ -742,9 +733,15 @@ class IptablesFirewallDriver(firewall.FirewallDriver):
742 733
             # icmp code can be 0 so we cannot use "if port_range_max" here
743 734
             if port_range_max is not None:
744 735
                 args[-1] += '/%s' % port_range_max
745
-        elif protocol in IPTABLES_PORT_PROTOCOLS:
736
+        elif protocol in n_const.SG_PORT_PROTO_NAMES:
737
+            # iptables protocols that support --dport, --sport and -m multiport
746 738
             if port_range_min == port_range_max:
747
-                args += ['--%s' % direction, '%s' % (port_range_min,)]
739
+                if protocol in n_const.IPTABLES_MULTIPORT_ONLY_PROTOCOLS:
740
+                    # use -m multiport, but without a port range
741
+                    args += ['-m', 'multiport', '--%ss' % direction,
742
+                             '%s' % port_range_min]
743
+                else:
744
+                    args += ['--%s' % direction, '%s' % port_range_min]
748 745
             else:
749 746
                 args += ['-m', 'multiport', '--%ss' % direction,
750 747
                          '%s:%s' % (port_range_min, port_range_max)]

+ 22
- 0
neutron/common/constants.py View File

@@ -134,6 +134,28 @@ IPTABLES_PROTOCOL_NAME_MAP = {lib_constants.PROTO_NAME_IPV6_ENCAP: 'ipv6',
134 134
                               '141': 'wesp',
135 135
                               '142': 'rohc'}
136 136
 
137
+# Security group protocols that support ports
138
+SG_PORT_PROTO_NUMS = [
139
+    lib_constants.PROTO_NUM_DCCP,
140
+    lib_constants.PROTO_NUM_SCTP,
141
+    lib_constants.PROTO_NUM_TCP,
142
+    lib_constants.PROTO_NUM_UDP,
143
+    lib_constants.PROTO_NUM_UDPLITE
144
+]
145
+
146
+SG_PORT_PROTO_NAMES = [
147
+    lib_constants.PROTO_NAME_DCCP,
148
+    lib_constants.PROTO_NAME_SCTP,
149
+    lib_constants.PROTO_NAME_TCP,
150
+    lib_constants.PROTO_NAME_UDP,
151
+    lib_constants.PROTO_NAME_UDPLITE
152
+]
153
+
154
+# iptables protocols that only support --dport and --sport using -m multiport
155
+IPTABLES_MULTIPORT_ONLY_PROTOCOLS = [
156
+    lib_constants.PROTO_NAME_UDPLITE
157
+]
158
+
137 159
 # A length of a iptables chain name must be less than or equal to 11
138 160
 # characters.
139 161
 # <max length of iptables chain name> - (<binary_name> + '-') = 28-(16+1) = 11

+ 10
- 10
neutron/db/securitygroups_db.py View File

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

+ 3
- 4
neutron/extensions/securitygroup.py View File

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

+ 26
- 0
neutron/tests/unit/agent/linux/test_iptables_firewall.py View File

@@ -415,6 +415,32 @@ class IptablesFirewallTestCase(BaseIptablesFirewallTestCase):
415 415
         egress = None
416 416
         self._test_prepare_port_filter(rule, ingress, egress)
417 417
 
418
+    def test_filter_ipv4_ingress_udplite_port(self):
419
+        rule = {'ethertype': 'IPv4',
420
+                'direction': 'ingress',
421
+                'protocol': 'udplite',
422
+                'port_range_min': 10,
423
+                'port_range_max': 10}
424
+        ingress = mock.call.add_rule(
425
+            'ifake_dev',
426
+            '-p udplite -m multiport --dports 10 -j RETURN',
427
+            top=False, comment=None)
428
+        egress = None
429
+        self._test_prepare_port_filter(rule, ingress, egress)
430
+
431
+    def test_filter_ipv4_ingress_udplite_mport(self):
432
+        rule = {'ethertype': 'IPv4',
433
+                'direction': 'ingress',
434
+                'protocol': 'udplite',
435
+                'port_range_min': 10,
436
+                'port_range_max': 100}
437
+        ingress = mock.call.add_rule(
438
+            'ifake_dev',
439
+            '-p udplite -m multiport --dports 10:100 -j RETURN',
440
+            top=False, comment=None)
441
+        egress = None
442
+        self._test_prepare_port_filter(rule, ingress, egress)
443
+
418 444
     def test_filter_ipv4_ingress_protocol_blank(self):
419 445
         rule = {'ethertype': 'IPv4',
420 446
                 'direction': 'ingress',

+ 13
- 1
neutron/tests/unit/db/test_securitygroups_db.py View File

@@ -460,8 +460,20 @@ class SecurityGroupDbMixinTestCase(testlib_api.SqlTestCase):
460 460
                            'port_range_max': 1,
461 461
                            'protocol': constants.PROTO_NAME_UDPLITE})
462 462
         self.assertRaises(
463
-            securitygroup.SecurityGroupInvalidProtocolForPortRange,
463
+            securitygroup.SecurityGroupInvalidProtocolForPort,
464 464
             self.mixin._validate_port_range,
465 465
             {'port_range_min': 100,
466 466
              'port_range_max': 200,
467 467
              'protocol': '111'})
468
+        self.assertRaises(
469
+            securitygroup.SecurityGroupInvalidProtocolForPort,
470
+            self.mixin._validate_port_range,
471
+            {'port_range_min': 100,
472
+             'port_range_max': None,
473
+             'protocol': constants.PROTO_NAME_VRRP})
474
+        self.assertRaises(
475
+            securitygroup.SecurityGroupInvalidProtocolForPort,
476
+            self.mixin._validate_port_range,
477
+            {'port_range_min': None,
478
+             'port_range_max': 200,
479
+             'protocol': constants.PROTO_NAME_VRRP})

+ 9
- 8
neutron/tests/unit/extensions/test_securitygroup.py View File

@@ -608,17 +608,18 @@ class TestSecurityGroups(SecurityGroupDBTestCase):
608 608
             self.deserialize(self.fmt, res)
609 609
             self.assertEqual(webob.exc.HTTPCreated.code, res.status_int)
610 610
 
611
-    def test_create_security_group_rule_protocol_as_number_with_port(self):
611
+    def test_create_security_group_rule_protocol_as_number_with_port_bad(self):
612
+        # When specifying ports, neither can be None
612 613
         name = 'webservers'
613 614
         description = 'my webservers'
614 615
         with self.security_group(name, description) as sg:
615 616
             security_group_id = sg['security_group']['id']
616
-            protocol = 111
617
+            protocol = 6
617 618
             rule = self._build_security_group_rule(
618
-                security_group_id, 'ingress', protocol, '70')
619
+                security_group_id, 'ingress', protocol, '70', None)
619 620
             res = self._create_security_group_rule(self.fmt, rule)
620 621
             self.deserialize(self.fmt, res)
621
-            self.assertEqual(webob.exc.HTTPCreated.code, res.status_int)
622
+            self.assertEqual(webob.exc.HTTPBadRequest.code, res.status_int)
622 623
 
623 624
     def test_create_security_group_rule_protocol_as_number_range(self):
624 625
         # This is a SG rule with a port range, but treated as a single
@@ -627,22 +628,22 @@ class TestSecurityGroups(SecurityGroupDBTestCase):
627 628
         description = 'my webservers'
628 629
         with self.security_group(name, description) as sg:
629 630
             security_group_id = sg['security_group']['id']
630
-            protocol = 111
631
+            protocol = 6
631 632
             rule = self._build_security_group_rule(
632 633
                 security_group_id, 'ingress', protocol, '70', '70')
633 634
             res = self._create_security_group_rule(self.fmt, rule)
634 635
             self.deserialize(self.fmt, res)
635 636
             self.assertEqual(webob.exc.HTTPCreated.code, res.status_int)
636 637
 
637
-    def test_create_security_group_rule_protocol_as_number_range_bad(self):
638
-        # Only certain protocols support a SG rule with a port range
638
+    def test_create_security_group_rule_protocol_as_number_port_bad(self):
639
+        # Only certain protocols support a SG rule with a port
639 640
         name = 'webservers'
640 641
         description = 'my webservers'
641 642
         with self.security_group(name, description) as sg:
642 643
             security_group_id = sg['security_group']['id']
643 644
             protocol = 111
644 645
             rule = self._build_security_group_rule(
645
-                security_group_id, 'ingress', protocol, '70', '71')
646
+                security_group_id, 'ingress', protocol, '70', '70')
646 647
             res = self._create_security_group_rule(self.fmt, rule)
647 648
             self.deserialize(self.fmt, res)
648 649
             self.assertEqual(webob.exc.HTTPBadRequest.code, res.status_int)

+ 11
- 0
releasenotes/notes/stricter-security-group-port-check-in-api-d1fd84d9663e04ab.yaml View File

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

Loading…
Cancel
Save