From 1d1159bb2b57f0b4193f8666f53736f05bf7eac9 Mon Sep 17 00:00:00 2001 From: Dustin Lundquist Date: Thu, 31 Mar 2016 15:19:04 -0700 Subject: [PATCH] IPtables firewall prevent ICMPv6 spoofing IPv6 includes the concept of link-local addresses. There are address within the fe80::/64 prefix which are used only within the local layer 2 network. They should never be routed. DHCPv6 is one of several protocols which utilize link-local addresses. Previously the blanket permit DHCPv6 rule permitted DHCPv6 requests from a link-local source, before the source address was validated. The structure of the IPtables egress firewall is: a. fixed rules for special traffic b. validate source address c. fixed rules necessary for host to function d. user rules defined by security groups This change restricts the special traffic permitted in part (a) to only that traffic which utilizes the "unspecified address" (::), by moving the fixed permit ICMPv6 and DHCPv6 rules to part (c), so they are applied after the source address has been validated. In order to enable DHCPv6 and other protocols utilizing link-local addresses, the link-local address corresponding to each MAC address are included in the permitted source addresses. After the source address is verified, the fixed rules permit ICMPv6 and DHCPv6, then the user defined security group rules are applied. In the existing implementation ICMPv6 and DHCPv6 rules in the fixed ip6tables firewall rules are too permissive: they permit ICMPv6 and DHCPv6 traffic, regardless of source MAC or IPv6 address. These rules where intended to allow a host to acquire an IPv6 address, but inadvertently allowed a malicious or compromised host to spoof another's MAC or IPv6 address. A host acquiring an IPv6 address should preform DAD (duplicate address detection). To preform this the host must join the multicast group corresponding to the tentative IPv6 address and the all nodes multicast group. To join these groups the host sends ICMP MLD (multicast listener discovery) report messages before it has an IPv6 address assigned, so the unspecified address is used as the source address. To complete DAD, ICMP neighbor solicitation messages are sent to solicit if any nodes using that address. This should be the only use of the unspecified IPv6 address as a source address. The IPv4 case is similar the unspecified address is used for DHCP discovery and request messages. To summarize, this patch permits only ICMPv6 traffic from the unspecified address which is used for duplicate address detection. Then it enforces the source IPv6 and MAC addresses and finally, allows only ICMPv6 traffic which has passed this source address validation. In addition this patch permits traffic from all link-local addresses associated with each MAC address assigned to the port. This is required by many IPv6 protocols, such as DHCPv6, which depend on the link-local addresses. This traffic was previously allowed by the blanket allow ICMPv6 and allow DHCPv6 rules before the source address was validated. Conflicts: neutron/agent/linux/iptables_firewall.py neutron/tests/functional/agent/test_firewall.py neutron/tests/unit/agent/linux/test_iptables_firewall.py Removed functional test not present in Liberty. Since Liberty doesn't contain the OVSFirewall this resolve bug 1502933 for Liberty. Change-Id: Ice1c9dd349864da28806c5053e38ef86f43b7771 Closes-Bug: 1502933 (cherry picked from commit a8a9d225d8496c044db7057552394afd6c950a8e) --- neutron/agent/linux/iptables_firewall.py | 33 +++++++--- .../agent/linux/test_iptables_firewall.py | 45 ++++++++++---- .../unit/agent/test_securitygroups_rpc.py | 60 ++++++++++++------- 3 files changed, 96 insertions(+), 42 deletions(-) diff --git a/neutron/agent/linux/iptables_firewall.py b/neutron/agent/linux/iptables_firewall.py index d985de8b1c3..681f46f910c 100644 --- a/neutron/agent/linux/iptables_firewall.py +++ b/neutron/agent/linux/iptables_firewall.py @@ -43,6 +43,7 @@ CHAIN_NAME_PREFIX = {firewall.INGRESS_DIRECTION: 'i', SPOOF_FILTER: 's'} DIRECTION_IP_PREFIX = {firewall.INGRESS_DIRECTION: 'source_ip_prefix', firewall.EGRESS_DIRECTION: 'dest_ip_prefix'} +ICMPV6_ALLOWED_UNSPEC_ADDR_TYPES = [131, 135, 143] IPSET_DIRECTION = {firewall.INGRESS_DIRECTION: 'src', firewall.EGRESS_DIRECTION: 'dst'} # length of all device prefixes (e.g. qvo, tap, qvb) @@ -384,21 +385,25 @@ class IptablesFirewallDriver(firewall.FirewallDriver): mac_ipv4_pairs.append((mac, ip_address)) else: mac_ipv6_pairs.append((mac, ip_address)) + lla = str(ipv6_utils.get_ipv6_addr_by_EUI64( + constants.IPV6_LLA_PREFIX, mac)) + mac_ipv6_pairs.append((mac, lla)) def _spoofing_rule(self, port, ipv4_rules, ipv6_rules): + # Fixed rules for traffic sourced from unspecified addresses: 0.0.0.0 + # and :: # Allow dhcp client discovery and request ipv4_rules += [comment_rule('-s 0.0.0.0/32 -d 255.255.255.255/32 ' '-p udp -m udp --sport 68 --dport 67 ' '-j RETURN', comment=ic.DHCP_CLIENT)] - # Drop Router Advts from the port. - ipv6_rules += [comment_rule('-p ipv6-icmp -m icmp6 --icmpv6-type %s ' - '-j DROP' % constants.ICMPV6_TYPE_RA, - comment=ic.IPV6_RA_DROP)] - ipv6_rules += [comment_rule('-p ipv6-icmp -j RETURN', - comment=ic.IPV6_ICMP_ALLOW)] - ipv6_rules += [comment_rule('-p udp -m udp --sport 546 ' - '-m udp --dport 547 ' - '-j RETURN', comment=ic.DHCP_CLIENT)] + # Allow neighbor solicitation and multicast listener discovery + # from the unspecified address for duplicate address detection + for icmp6_type in ICMPV6_ALLOWED_UNSPEC_ADDR_TYPES: + ipv6_rules += [comment_rule('-s ::/128 -d ff02::/16 ' + '-p ipv6-icmp -m icmp6 ' + '--icmpv6-type %s -j RETURN' % + icmp6_type, + comment=ic.IPV6_ICMP_ALLOW)] mac_ipv4_pairs = [] mac_ipv6_pairs = [] @@ -420,9 +425,19 @@ class IptablesFirewallDriver(firewall.FirewallDriver): mac_ipv4_pairs, ipv4_rules) self._setup_spoof_filter_chain(port, self.iptables.ipv6['filter'], mac_ipv6_pairs, ipv6_rules) + # Fixed rules for traffic after source address is verified # Allow dhcp client renewal and rebinding ipv4_rules += [comment_rule('-p udp -m udp --sport 68 --dport 67 ' '-j RETURN', comment=ic.DHCP_CLIENT)] + # Drop Router Advts from the port. + ipv6_rules += [comment_rule('-p ipv6-icmp -m icmp6 --icmpv6-type %s ' + '-j DROP' % constants.ICMPV6_TYPE_RA, + comment=ic.IPV6_RA_DROP)] + ipv6_rules += [comment_rule('-p ipv6-icmp -j RETURN', + comment=ic.IPV6_ICMP_ALLOW)] + ipv6_rules += [comment_rule('-p udp -m udp --sport 546 ' + '-m udp --dport 547 ' + '-j RETURN', comment=ic.DHCP_CLIENT)] def _drop_dhcp_rule(self, ipv4_rules, ipv6_rules): #Note(nati) Drop dhcp packet from VM diff --git a/neutron/tests/unit/agent/linux/test_iptables_firewall.py b/neutron/tests/unit/agent/linux/test_iptables_firewall.py index d413cb6b1db..fd7e408b915 100644 --- a/neutron/tests/unit/agent/linux/test_iptables_firewall.py +++ b/neutron/tests/unit/agent/linux/test_iptables_firewall.py @@ -952,17 +952,21 @@ class IptablesFirewallTestCase(BaseIptablesFirewallTestCase): if ethertype == 'IPv6': filter_inst = self.v6filter_inst - dhcp_rule = [mock.call.add_rule('ofake_dev', '-p ipv6-icmp ' - '-m icmp6 ' - '--icmpv6-type %s -j DROP' - % constants.ICMPV6_TYPE_RA, + dhcp_rule = [mock.call.add_rule('ofake_dev', + '-s ::/128 -d ff02::/16 ' + '-p ipv6-icmp -m icmp6 ' + '--icmpv6-type 131 -j RETURN', comment=None), mock.call.add_rule('ofake_dev', - '-p ipv6-icmp -j RETURN', + '-s ::/128 -d ff02::/16 ' + '-p ipv6-icmp -m icmp6 ' + '--icmpv6-type 135 -j RETURN', comment=None), - mock.call.add_rule('ofake_dev', '-p udp -m udp ' - '--sport 546 -m udp --dport 547 ' - '-j RETURN', comment=None)] + mock.call.add_rule('ofake_dev', + '-s ::/128 -d ff02::/16 ' + '-p ipv6-icmp -m icmp6 ' + '--icmpv6-type 143 -j RETURN', + comment=None)] sg = [rule] port['security_group_rules'] = sg self.firewall.prepare_port_filter(port) @@ -1025,10 +1029,15 @@ class IptablesFirewallTestCase(BaseIptablesFirewallTestCase): 'sfake_dev', '-s %s -m mac --mac-source FF:FF:FF:FF:FF:FF -j RETURN' % prefix, - comment=ic.PAIR_ALLOW), - mock.call.add_rule( - 'sfake_dev', '-j DROP', - comment=ic.PAIR_DROP)] + comment=ic.PAIR_ALLOW)] + + if ethertype == 'IPv6': + calls.append(mock.call.add_rule('sfake_dev', + '-s fe80::fdff:ffff:feff:ffff/128 -m mac ' + '--mac-source FF:FF:FF:FF:FF:FF -j RETURN', + comment=ic.PAIR_ALLOW)) + calls.append(mock.call.add_rule('sfake_dev', '-j DROP', + comment=ic.PAIR_DROP)) calls += dhcp_rule calls.append(mock.call.add_rule('ofake_dev', '-j $sfake_dev', comment=None)) @@ -1042,6 +1051,17 @@ class IptablesFirewallTestCase(BaseIptablesFirewallTestCase): '-p udp -m udp --sport 67 -m udp --dport 68 -j DROP', comment=None)) if ethertype == 'IPv6': + calls.append(mock.call.add_rule('ofake_dev', + '-p ipv6-icmp -m icmp6 ' + '--icmpv6-type %s -j DROP' % + constants.ICMPV6_TYPE_RA, + comment=None)) + calls.append(mock.call.add_rule('ofake_dev', + '-p ipv6-icmp -j RETURN', + comment=None)) + calls.append(mock.call.add_rule('ofake_dev', '-p udp -m udp ' + '--sport 546 -m udp --dport 547 ' + '-j RETURN', comment=None)) calls.append(mock.call.add_rule( 'ofake_dev', '-p udp -m udp --sport 547 -m udp --dport 546 -j DROP', @@ -1862,6 +1882,7 @@ class IptablesFirewallEnhancedIpsetTestCase(BaseIptablesFirewallTestCase): fake_ipv4_pair.append((mac_unix, ipv4)) fake_ipv6_pair = [] fake_ipv6_pair.append((mac_unix, ipv6)) + fake_ipv6_pair.append((mac_unix, 'fe80::fdff:ffff:fe0f:ffff')) mac_ipv4_pairs = [] mac_ipv6_pairs = [] diff --git a/neutron/tests/unit/agent/test_securitygroups_rpc.py b/neutron/tests/unit/agent/test_securitygroups_rpc.py index 7b10ac956b9..60406b0a3af 100644 --- a/neutron/tests/unit/agent/test_securitygroups_rpc.py +++ b/neutron/tests/unit/agent/test_securitygroups_rpc.py @@ -2440,13 +2440,19 @@ IPTABLES_FILTER_V6_1 = """# Generated by iptables_manager -I %(bn)s-i_port1 6 -m state --state RELATED,ESTABLISHED -j RETURN -I %(bn)s-i_port1 7 -m state --state INVALID -j DROP -I %(bn)s-i_port1 8 -j %(bn)s-sg-fallback --I %(bn)s-o_port1 1 -p ipv6-icmp -m icmp6 --icmpv6-type 134 -j DROP --I %(bn)s-o_port1 2 -p ipv6-icmp -j RETURN --I %(bn)s-o_port1 3 -p udp -m udp --sport 546 -m udp --dport 547 -j RETURN --I %(bn)s-o_port1 4 -p udp -m udp --sport 547 -m udp --dport 546 -j DROP --I %(bn)s-o_port1 5 -m state --state RELATED,ESTABLISHED -j RETURN --I %(bn)s-o_port1 6 -m state --state INVALID -j DROP --I %(bn)s-o_port1 7 -j %(bn)s-sg-fallback +-I %(bn)s-o_port1 1 -s ::/128 -d ff02::/16 -p ipv6-icmp -m icmp6 \ +--icmpv6-type 131 -j RETURN +-I %(bn)s-o_port1 2 -s ::/128 -d ff02::/16 -p ipv6-icmp -m icmp6 \ +--icmpv6-type 135 -j RETURN +-I %(bn)s-o_port1 3 -s ::/128 -d ff02::/16 -p ipv6-icmp -m icmp6 \ +--icmpv6-type 143 -j RETURN +-I %(bn)s-o_port1 4 -p ipv6-icmp -m icmp6 --icmpv6-type 134 -j DROP +-I %(bn)s-o_port1 5 -p ipv6-icmp -j RETURN +-I %(bn)s-o_port1 6 -p udp -m udp --sport 546 -m udp --dport 547 -j RETURN +-I %(bn)s-o_port1 7 -p udp -m udp --sport 547 -m udp --dport 546 -j DROP +-I %(bn)s-o_port1 8 -m state --state RELATED,ESTABLISHED -j RETURN +-I %(bn)s-o_port1 9 -m state --state INVALID -j DROP +-I %(bn)s-o_port1 10 -j %(bn)s-sg-fallback -I %(bn)s-sg-chain 1 %(physdev_mod)s --physdev-INGRESS tap_port1 \ %(physdev_is_bridged)s -j %(bn)s-i_port1 -I %(bn)s-sg-chain 2 %(physdev_mod)s --physdev-EGRESS tap_port1 \ @@ -2510,20 +2516,32 @@ IPTABLES_FILTER_V6_2 = """# Generated by iptables_manager -I %(bn)s-i_%(port2)s 6 -m state --state RELATED,ESTABLISHED -j RETURN -I %(bn)s-i_%(port2)s 7 -m state --state INVALID -j DROP -I %(bn)s-i_%(port2)s 8 -j %(bn)s-sg-fallback --I %(bn)s-o_%(port1)s 1 -p ipv6-icmp -m icmp6 --icmpv6-type 134 -j DROP --I %(bn)s-o_%(port1)s 2 -p ipv6-icmp -j RETURN --I %(bn)s-o_%(port1)s 3 -p udp -m udp --sport 546 -m udp --dport 547 -j RETURN --I %(bn)s-o_%(port1)s 4 -p udp -m udp --sport 547 -m udp --dport 546 -j DROP --I %(bn)s-o_%(port1)s 5 -m state --state RELATED,ESTABLISHED -j RETURN --I %(bn)s-o_%(port1)s 6 -m state --state INVALID -j DROP --I %(bn)s-o_%(port1)s 7 -j %(bn)s-sg-fallback --I %(bn)s-o_%(port2)s 1 -p ipv6-icmp -m icmp6 --icmpv6-type 134 -j DROP --I %(bn)s-o_%(port2)s 2 -p ipv6-icmp -j RETURN --I %(bn)s-o_%(port2)s 3 -p udp -m udp --sport 546 -m udp --dport 547 -j RETURN --I %(bn)s-o_%(port2)s 4 -p udp -m udp --sport 547 -m udp --dport 546 -j DROP --I %(bn)s-o_%(port2)s 5 -m state --state RELATED,ESTABLISHED -j RETURN --I %(bn)s-o_%(port2)s 6 -m state --state INVALID -j DROP --I %(bn)s-o_%(port2)s 7 -j %(bn)s-sg-fallback +-I %(bn)s-o_%(port1)s 1 -s ::/128 -d ff02::/16 -p ipv6-icmp -m icmp6 \ +--icmpv6-type 131 -j RETURN +-I %(bn)s-o_%(port1)s 2 -s ::/128 -d ff02::/16 -p ipv6-icmp -m icmp6 \ +--icmpv6-type 135 -j RETURN +-I %(bn)s-o_%(port1)s 3 -s ::/128 -d ff02::/16 -p ipv6-icmp -m icmp6 \ +--icmpv6-type 143 -j RETURN +-I %(bn)s-o_%(port1)s 4 -p ipv6-icmp -m icmp6 --icmpv6-type 134 -j DROP +-I %(bn)s-o_%(port1)s 5 -p ipv6-icmp -j RETURN +-I %(bn)s-o_%(port1)s 6 -p udp -m udp --sport 546 -m udp --dport 547 -j RETURN +-I %(bn)s-o_%(port1)s 7 -p udp -m udp --sport 547 -m udp --dport 546 -j DROP +-I %(bn)s-o_%(port1)s 8 -m state --state RELATED,ESTABLISHED -j RETURN +-I %(bn)s-o_%(port1)s 9 -m state --state INVALID -j DROP +-I %(bn)s-o_%(port1)s 10 -j %(bn)s-sg-fallback +-I %(bn)s-o_%(port2)s 1 -s ::/128 -d ff02::/16 -p ipv6-icmp -m icmp6 \ +--icmpv6-type 131 -j RETURN +-I %(bn)s-o_%(port2)s 2 -s ::/128 -d ff02::/16 -p ipv6-icmp -m icmp6 \ +--icmpv6-type 135 -j RETURN +-I %(bn)s-o_%(port2)s 3 -s ::/128 -d ff02::/16 -p ipv6-icmp -m icmp6 \ +--icmpv6-type 143 -j RETURN +-I %(bn)s-o_%(port2)s 4 -p ipv6-icmp -m icmp6 --icmpv6-type 134 -j DROP +-I %(bn)s-o_%(port2)s 5 -p ipv6-icmp -j RETURN +-I %(bn)s-o_%(port2)s 6 -p udp -m udp --sport 546 -m udp --dport 547 -j RETURN +-I %(bn)s-o_%(port2)s 7 -p udp -m udp --sport 547 -m udp --dport 546 -j DROP +-I %(bn)s-o_%(port2)s 8 -m state --state RELATED,ESTABLISHED -j RETURN +-I %(bn)s-o_%(port2)s 9 -m state --state INVALID -j DROP +-I %(bn)s-o_%(port2)s 10 -j %(bn)s-sg-fallback -I %(bn)s-sg-chain 1 %(physdev_mod)s --physdev-INGRESS tap_%(port1)s \ %(physdev_is_bridged)s -j %(bn)s-i_%(port1)s -I %(bn)s-sg-chain 2 %(physdev_mod)s --physdev-EGRESS tap_%(port1)s \