From cb746e2ca454885ba5cf653493b9bb0e91480759 Mon Sep 17 00:00:00 2001 From: Slawek Kaplonski Date: Mon, 29 Mar 2021 22:21:15 +0200 Subject: [PATCH] [ovs fw] Restrict IPv6 NA and DHCP(v6) IP and MAC source addresses Neighbor Advertisments are used to inform other machines of the MAC address to use to reach an IPv6. This commits prevents VMs from pretending they are assigned IPv6 they should not use. It also prevents sending UDP packets with spoofed IP or MAC even using DHCP(v6) request ports. Co-authored-by: David Sinquin Closes-bug: #1902917 Change-Id: Iffb6643359562487414460f5a7e19a7fae9f935c (cherry picked from commit ca7822e2108c151bda992ef8a6d454ec2c6d890e) --- neutron/agent/firewall.py | 7 +- .../linux/openvswitch_firewall/firewall.py | 65 ++++++++++++------- .../openvswitch_firewall/test_firewall.py | 13 ++++ 3 files changed, 61 insertions(+), 24 deletions(-) diff --git a/neutron/agent/firewall.py b/neutron/agent/firewall.py index f8f66154a46..62eee300e79 100644 --- a/neutron/agent/firewall.py +++ b/neutron/agent/firewall.py @@ -34,8 +34,11 @@ ICMPV6_ALLOWED_INGRESS_TYPES = (n_const.ICMPV6_TYPE_MLD_QUERY, # List of ICMPv6 types that should be permitted (egress) by default. ICMPV6_ALLOWED_EGRESS_TYPES = (n_const.ICMPV6_TYPE_MLD_QUERY, n_const.ICMPV6_TYPE_RS, - n_const.ICMPV6_TYPE_NS, - n_const.ICMPV6_TYPE_NA) + n_const.ICMPV6_TYPE_NS) + +# List of ICMPv6 types that should be permitted depending on payload content +# to avoid spoofing (egress) by default. +ICMPV6_RESTRICTED_EGRESS_TYPES = (n_const.ICMPV6_TYPE_NA, ) def port_sec_enabled(port): diff --git a/neutron/agent/linux/openvswitch_firewall/firewall.py b/neutron/agent/linux/openvswitch_firewall/firewall.py index 4f7c2cc19da..5d3ec14a96f 100644 --- a/neutron/agent/linux/openvswitch_firewall/firewall.py +++ b/neutron/agent/linux/openvswitch_firewall/firewall.py @@ -934,8 +934,7 @@ class OVSFirewallDriver(firewall.FirewallDriver): self._initialize_ingress(port) def _initialize_egress_ipv6_icmp(self, port, allowed_pairs): - # NOTE(slaweq): should we include also fe80::/64 (link-local) subnet - # in the allowed pairs here? + allowed_pairs = allowed_pairs.union({(port.mac, port.lla_address)}) for mac_addr, ip_addr in allowed_pairs: for icmp_type in firewall.ICMPV6_ALLOWED_EGRESS_TYPES: self._add_flow( @@ -951,6 +950,19 @@ class OVSFirewallDriver(firewall.FirewallDriver): actions='resubmit(,%d)' % ( ovs_consts.ACCEPTED_EGRESS_TRAFFIC_NORMAL_TABLE) ) + for icmp_type in firewall.ICMPV6_RESTRICTED_EGRESS_TYPES: + self._add_flow( + table=ovs_consts.BASE_EGRESS_TABLE, + priority=95, + in_port=port.ofport, + reg_port=port.ofport, + dl_type=lib_const.ETHERTYPE_IPV6, + nw_proto=lib_const.PROTO_NUM_IPV6_ICMP, + icmp_type=icmp_type, + nd_target=ip_addr, + actions='resubmit(,%d)' % ( + ovs_consts.ACCEPTED_EGRESS_TRAFFIC_NORMAL_TABLE) + ) def _initialize_egress_no_port_security(self, port_id, ovs_ports=None): try: @@ -1025,9 +1037,9 @@ class OVSFirewallDriver(firewall.FirewallDriver): """Identify egress traffic and send it to egress base""" # Apply mac/ip pairs for IPv4 - allowed_pairs = port.allowed_pairs_v4.union( + allowed_mac_ipv4_pairs = port.allowed_pairs_v4.union( {(port.mac, ip_addr) for ip_addr in port.ipv4_addresses}) - for mac_addr, ip_addr in allowed_pairs: + for mac_addr, ip_addr in allowed_mac_ipv4_pairs: self._add_flow( table=ovs_consts.BASE_EGRESS_TABLE, priority=95, @@ -1053,10 +1065,10 @@ class OVSFirewallDriver(firewall.FirewallDriver): ) # Apply mac/ip pairs for IPv6 - allowed_pairs = port.allowed_pairs_v6.union( + allowed_mac_ipv6_pairs = port.allowed_pairs_v6.union( {(port.mac, ip_addr) for ip_addr in port.ipv6_addresses}) - self._initialize_egress_ipv6_icmp(port, allowed_pairs) - for mac_addr, ip_addr in allowed_pairs: + self._initialize_egress_ipv6_icmp(port, allowed_mac_ipv6_pairs) + for mac_addr, ip_addr in allowed_mac_ipv6_pairs: self._add_flow( table=ovs_consts.BASE_EGRESS_TABLE, priority=65, @@ -1071,21 +1083,30 @@ class OVSFirewallDriver(firewall.FirewallDriver): ) # DHCP discovery - for dl_type, src_port, dst_port in ( - (lib_const.ETHERTYPE_IP, 68, 67), - (lib_const.ETHERTYPE_IPV6, 546, 547)): - self._add_flow( - table=ovs_consts.BASE_EGRESS_TABLE, - priority=80, - reg_port=port.ofport, - in_port=port.ofport, - dl_type=dl_type, - nw_proto=lib_const.PROTO_NUM_UDP, - tp_src=src_port, - tp_dst=dst_port, - actions='resubmit(,{:d})'.format( - ovs_consts.ACCEPT_OR_INGRESS_TABLE) - ) + additional_ipv4_filters = [ + {"dl_src": mac, "nw_src": ip} + for mac, ip in (*allowed_mac_ipv4_pairs, + (port.mac, '0.0.0.0'),)] + additional_ipv6_filters = [ + {"dl_src": mac, "ipv6_src": ip} + for mac, ip in allowed_mac_ipv6_pairs] + for dl_type, src_port, dst_port, filters_list in ( + (lib_const.ETHERTYPE_IP, 68, 67, additional_ipv4_filters), + (lib_const.ETHERTYPE_IPV6, 546, 547, additional_ipv6_filters)): + for additional_filters in filters_list: + self._add_flow( + table=ovs_consts.BASE_EGRESS_TABLE, + priority=80, + reg_port=port.ofport, + in_port=port.ofport, + dl_type=dl_type, + **additional_filters, + nw_proto=lib_const.PROTO_NUM_UDP, + tp_src=src_port, + tp_dst=dst_port, + actions='resubmit(,{:d})'.format( + ovs_consts.ACCEPT_OR_INGRESS_TABLE) + ) # Ban dhcp service running on an instance for dl_type, src_port, dst_port in ( (lib_const.ETHERTYPE_IP, 67, 68), diff --git a/neutron/tests/unit/agent/linux/openvswitch_firewall/test_firewall.py b/neutron/tests/unit/agent/linux/openvswitch_firewall/test_firewall.py index 5bdfe1f0677..ab3d1707963 100644 --- a/neutron/tests/unit/agent/linux/openvswitch_firewall/test_firewall.py +++ b/neutron/tests/unit/agent/linux/openvswitch_firewall/test_firewall.py @@ -1057,6 +1057,19 @@ class TestOVSFirewallDriver(base.BaseTestCase): ipv6_src='2003::1', actions='resubmit(,%d)' % ( ovs_consts.ACCEPTED_EGRESS_TRAFFIC_NORMAL_TABLE))) + for icmp_type in agent_firewall.ICMPV6_RESTRICTED_EGRESS_TYPES: + expected_calls.append( + mock.call( + table=ovs_consts.BASE_EGRESS_TABLE, + priority=95, + in_port=TESTING_VLAN_TAG, + reg5=TESTING_VLAN_TAG, + dl_type='0x86dd', + nw_proto=constants.PROTO_NUM_IPV6_ICMP, + icmp_type=icmp_type, + nd_target='2003::1', + actions='resubmit(,%d)' % ( + ovs_consts.ACCEPTED_EGRESS_TRAFFIC_NORMAL_TABLE))) self.mock_bridge.br.add_flow.assert_has_calls(expected_calls) def test_process_trusted_ports_caches_port_id(self):