diff --git a/neutron/plugins/ml2/drivers/openvswitch/agent/common/constants.py b/neutron/plugins/ml2/drivers/openvswitch/agent/common/constants.py index 72ba2e2bbe2..96903d35fac 100644 --- a/neutron/plugins/ml2/drivers/openvswitch/agent/common/constants.py +++ b/neutron/plugins/ml2/drivers/openvswitch/agent/common/constants.py @@ -50,6 +50,9 @@ CANARY_TABLE = 23 # Table for ARP poison/spoofing prevention rules ARP_SPOOF_TABLE = 24 +# Table for MAC spoof filtering +MAC_SPOOF_TABLE = 25 + # Tables used for ovs firewall BASE_EGRESS_TABLE = 71 RULES_EGRESS_TABLE = 72 diff --git a/neutron/plugins/ml2/drivers/openvswitch/agent/openflow/native/br_int.py b/neutron/plugins/ml2/drivers/openvswitch/agent/openflow/native/br_int.py index e7bfb0dcf97..5ac90784df3 100644 --- a/neutron/plugins/ml2/drivers/openvswitch/agent/openflow/native/br_int.py +++ b/neutron/plugins/ml2/drivers/openvswitch/agent/openflow/native/br_int.py @@ -19,6 +19,8 @@ ** OVS agent https://wiki.openstack.org/wiki/Ovs-flow-logic """ +import netaddr + from oslo_log import log as logging from ryu.lib.packet import ether_types from ryu.lib.packet import icmpv6 @@ -174,16 +176,45 @@ class OVSIntegrationBridge(ovs_bridge.OVSAgentBridge): match=match, dest_table_id=constants.ARP_SPOOF_TABLE) + def set_allowed_macs_for_port(self, port, mac_addresses=None, + allow_all=False): + if allow_all: + self.delete_flows(table_id=constants.LOCAL_SWITCHING, in_port=port) + self.delete_flows(table_id=constants.MAC_SPOOF_TABLE, in_port=port) + return + mac_addresses = mac_addresses or [] + for address in mac_addresses: + self.install_normal( + table_id=constants.MAC_SPOOF_TABLE, priority=2, + eth_src=address, in_port=port) + # normalize so we can see if macs are the same + mac_addresses = {netaddr.EUI(mac) for mac in mac_addresses} + flows = self.dump_flows(constants.MAC_SPOOF_TABLE) + for flow in flows: + matches = dict(flow.match.items()) + if matches.get('in_port') != port: + continue + if not matches.get('eth_src'): + continue + flow_mac = matches['eth_src'] + if netaddr.EUI(flow_mac) not in mac_addresses: + self.delete_flows(table_id=constants.MAC_SPOOF_TABLE, + in_port=port, eth_src=flow_mac) + self.install_goto(table_id=constants.LOCAL_SWITCHING, + priority=9, in_port=port, + dest_table_id=constants.MAC_SPOOF_TABLE) + def install_arp_spoofing_protection(self, port, ip_addresses): # allow ARP replies as long as they match addresses that actually # belong to the port. for ip in ip_addresses: masked_ip = self._cidr_to_ryu(ip) - self.install_normal(table_id=constants.ARP_SPOOF_TABLE, - priority=2, - eth_type=ether_types.ETH_TYPE_ARP, - arp_spa=masked_ip, - in_port=port) + self.install_goto(table_id=constants.ARP_SPOOF_TABLE, + priority=2, + eth_type=ether_types.ETH_TYPE_ARP, + arp_spa=masked_ip, + in_port=port, + dest_table_id=constants.MAC_SPOOF_TABLE) # Now that the rules are ready, direct ARP traffic from the port into # the anti-spoof table. diff --git a/neutron/plugins/ml2/drivers/openvswitch/agent/openflow/ovs_ofctl/br_int.py b/neutron/plugins/ml2/drivers/openvswitch/agent/openflow/ovs_ofctl/br_int.py index a7c55fc5e30..bc7a4eb423c 100644 --- a/neutron/plugins/ml2/drivers/openvswitch/agent/openflow/ovs_ofctl/br_int.py +++ b/neutron/plugins/ml2/drivers/openvswitch/agent/openflow/ovs_ofctl/br_int.py @@ -18,6 +18,9 @@ * references ** OVS agent https://wiki.openstack.org/wiki/Ovs-flow-logic """ + +import netaddr + from neutron.common import constants as const from neutron.plugins.common import constants as p_const from neutron.plugins.ml2.drivers.openvswitch.agent.common import constants @@ -128,13 +131,40 @@ class OVSIntegrationBridge(ovs_bridge.OVSAgentBridge): icmp_type=const.ICMPV6_TYPE_NA, in_port=port, actions=("resubmit(,%s)" % constants.ARP_SPOOF_TABLE)) + def set_allowed_macs_for_port(self, port, mac_addresses=None, + allow_all=False): + if allow_all: + self.delete_flows(table_id=constants.LOCAL_SWITCHING, in_port=port) + self.delete_flows(table_id=constants.MAC_SPOOF_TABLE, in_port=port) + return + mac_addresses = mac_addresses or [] + for address in mac_addresses: + self.install_normal( + table_id=constants.MAC_SPOOF_TABLE, priority=2, + eth_src=address, in_port=port) + # normalize so we can see if macs are the same + mac_addresses = {netaddr.EUI(mac) for mac in mac_addresses} + flows = self.dump_flows_for(table=constants.MAC_SPOOF_TABLE, + in_port=port).splitlines() + for flow in flows: + if 'dl_src' not in flow: + continue + flow_mac = flow.split('dl_src=')[1].split(' ')[0].split(',')[0] + if netaddr.EUI(flow_mac) not in mac_addresses: + self.delete_flows(table_id=constants.MAC_SPOOF_TABLE, + in_port=port, eth_src=flow_mac) + self.add_flow(table=constants.LOCAL_SWITCHING, + priority=9, in_port=port, + actions=("resubmit(,%s)" % constants.MAC_SPOOF_TABLE)) + def install_arp_spoofing_protection(self, port, ip_addresses): # allow ARPs as long as they match addresses that actually # belong to the port. for ip in ip_addresses: - self.install_normal( - table_id=constants.ARP_SPOOF_TABLE, priority=2, - proto='arp', arp_spa=ip, in_port=port) + self.add_flow( + table=constants.ARP_SPOOF_TABLE, priority=2, + proto='arp', arp_spa=ip, in_port=port, + actions=("resubmit(,%s)" % constants.MAC_SPOOF_TABLE)) # Now that the rules are ready, direct ARP traffic from the port into # the anti-spoof table. diff --git a/neutron/plugins/ml2/drivers/openvswitch/agent/ovs_neutron_agent.py b/neutron/plugins/ml2/drivers/openvswitch/agent/ovs_neutron_agent.py index 14867afe598..2b8cc66d426 100644 --- a/neutron/plugins/ml2/drivers/openvswitch/agent/ovs_neutron_agent.py +++ b/neutron/plugins/ml2/drivers/openvswitch/agent/ovs_neutron_agent.py @@ -885,12 +885,14 @@ class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin, LOG.info(_LI("Skipping ARP spoofing rules for port '%s' because " "it has port security disabled"), vif.port_name) bridge.delete_arp_spoofing_protection(port=vif.ofport) + bridge.set_allowed_macs_for_port(port=vif.ofport, allow_all=True) return if port_details['device_owner'].startswith( n_const.DEVICE_OWNER_NETWORK_PREFIX): LOG.debug("Skipping ARP spoofing rules for network owned port " "'%s'.", vif.port_name) bridge.delete_arp_spoofing_protection(port=vif.ofport) + bridge.set_allowed_macs_for_port(port=vif.ofport, allow_all=True) return # clear any previous flows related to this port in our ARP table bridge.delete_arp_spoofing_allow_rules(port=vif.ofport) @@ -904,6 +906,7 @@ class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin, for p in port_details['allowed_address_pairs'] if p.get('mac_address')} + bridge.set_allowed_macs_for_port(vif.ofport, mac_addresses) ipv6_addresses = {ip for ip in addresses if netaddr.IPNetwork(ip).version == 6} # Allow neighbor advertisements for LLA address. diff --git a/neutron/tests/functional/agent/test_ovs_flows.py b/neutron/tests/functional/agent/test_ovs_flows.py index d24cf2b99ca..d2854dc36b5 100644 --- a/neutron/tests/functional/agent/test_ovs_flows.py +++ b/neutron/tests/functional/agent/test_ovs_flows.py @@ -184,6 +184,17 @@ class ARPSpoofTestCase(OVSAgentTestBase): self.dst_p.addr.add('%s/24' % self.dst_addr) net_helpers.assert_ping(self.src_namespace, self.dst_addr, count=2) + def test_mac_spoof_blocks_wrong_mac(self): + self._setup_arp_spoof_for_port(self.src_p.name, [self.src_addr]) + self._setup_arp_spoof_for_port(self.dst_p.name, [self.dst_addr]) + self.src_p.addr.add('%s/24' % self.src_addr) + self.dst_p.addr.add('%s/24' % self.dst_addr) + net_helpers.assert_ping(self.src_namespace, self.dst_addr, count=2) + # changing the allowed mac should stop the port from working + self._setup_arp_spoof_for_port(self.src_p.name, [self.src_addr], + mac='00:11:22:33:44:55') + net_helpers.assert_no_ping(self.src_namespace, self.dst_addr, count=2) + def test_arp_spoof_doesnt_block_ipv6(self): self.src_addr = '2000::1' self.dst_addr = '2000::2' @@ -282,7 +293,7 @@ class ARPSpoofTestCase(OVSAgentTestBase): net_helpers.assert_ping(self.src_namespace, self.dst_addr, count=2) def _setup_arp_spoof_for_port(self, port, addrs, psec=True, - device_owner='nobody'): + device_owner='nobody', mac=None): vif = next( vif for vif in self.br.get_vif_ports() if vif.port_name == port) ip_addr = addrs.pop() @@ -291,6 +302,8 @@ class ARPSpoofTestCase(OVSAgentTestBase): 'device_owner': device_owner, 'allowed_address_pairs': [ dict(ip_address=ip) for ip in addrs]} + if mac: + vif.vif_mac = mac ovsagt.OVSNeutronAgent.setup_arp_spoofing_protection( self.br_int, vif, details) diff --git a/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/openflow/native/test_br_int.py b/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/openflow/native/test_br_int.py index 32086ff2751..13b56fb05ac 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/openflow/native/test_br_int.py +++ b/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/openflow/native/test_br_int.py @@ -347,9 +347,7 @@ class OVSIntegrationBridgeTest(ovs_bridge_test_base.OVSBridgeTestBase): call._send_msg(ofpp.OFPFlowMod(dp, cookie=self.stamp, instructions=[ - ofpp.OFPInstructionActions(ofp.OFPIT_APPLY_ACTIONS, [ - ofpp.OFPActionOutput(ofp.OFPP_NORMAL, 0), - ]), + ofpp.OFPInstructionGotoTable(table_id=25), ], match=ofpp.OFPMatch( eth_type=self.ether_types.ETH_TYPE_ARP, @@ -361,9 +359,7 @@ class OVSIntegrationBridgeTest(ovs_bridge_test_base.OVSBridgeTestBase): call._send_msg(ofpp.OFPFlowMod(dp, cookie=self.stamp, instructions=[ - ofpp.OFPInstructionActions(ofp.OFPIT_APPLY_ACTIONS, [ - ofpp.OFPActionOutput(ofp.OFPP_NORMAL, 0), - ]), + ofpp.OFPInstructionGotoTable(table_id=25), ], match=ofpp.OFPMatch( eth_type=self.ether_types.ETH_TYPE_ARP, diff --git a/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/openflow/ovs_ofctl/test_br_int.py b/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/openflow/ovs_ofctl/test_br_int.py index 51892fd10c0..5dc9a0c2ffb 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/openflow/ovs_ofctl/test_br_int.py +++ b/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/openflow/ovs_ofctl/test_br_int.py @@ -215,10 +215,10 @@ class OVSIntegrationBridgeTest(ovs_bridge_test_base.OVSBridgeTestBase): ip_addresses = ['192.0.2.1', '192.0.2.2/32'] self.br.install_arp_spoofing_protection(port, ip_addresses) expected = [ - call.add_flow(proto='arp', actions='normal', + call.add_flow(proto='arp', actions='resubmit(,25)', arp_spa='192.0.2.1', priority=2, table=24, in_port=8888), - call.add_flow(proto='arp', actions='normal', + call.add_flow(proto='arp', actions='resubmit(,25)', arp_spa='192.0.2.2/32', priority=2, table=24, in_port=8888), call.add_flow(priority=10, table=0, in_port=8888,