From 9f4a9f8f86720567a8ed5e7c5c4430c115ad3c11 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 Conflicts: neutron/agent/linux/openvswitch_firewall/firewall.py Change-Id: Iffb6643359562487414460f5a7e19a7fae9f935c (cherry picked from commit ca7822e2108c151bda992ef8a6d454ec2c6d890e) --- neutron/agent/firewall.py | 7 +- .../linux/openvswitch_firewall/firewall.py | 66 ++++++++++++------- .../openvswitch_firewall/test_firewall.py | 13 ++++ 3 files changed, 62 insertions(+), 24 deletions(-) diff --git a/neutron/agent/firewall.py b/neutron/agent/firewall.py index 4a54c6a2399..71df95dbd9f 100644 --- a/neutron/agent/firewall.py +++ b/neutron/agent/firewall.py @@ -36,8 +36,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 21a18852cbb..7be55734209 100644 --- a/neutron/agent/linux/openvswitch_firewall/firewall.py +++ b/neutron/agent/linux/openvswitch_firewall/firewall.py @@ -892,8 +892,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( @@ -909,6 +908,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=constants.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: @@ -983,9 +995,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, @@ -1011,10 +1023,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, @@ -1029,21 +1041,31 @@ class OVSFirewallDriver(firewall.FirewallDriver): ) # DHCP discovery - for dl_type, src_port, dst_port in ( - (constants.ETHERTYPE_IP, 68, 67), - (constants.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] + additional_ipv4_filters.append( + {"dl_src": port.mac, "nw_src": "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 ( + (constants.ETHERTYPE_IP, 68, 67, additional_ipv4_filters), + (constants.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, + 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_filters + ) # Ban dhcp service running on an instance for dl_type, src_port, dst_port in ( (constants.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 3196c3626da..faa4be75268 100644 --- a/neutron/tests/unit/agent/linux/openvswitch_firewall/test_firewall.py +++ b/neutron/tests/unit/agent/linux/openvswitch_firewall/test_firewall.py @@ -994,6 +994,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):