OVS: Add mac spoofing filtering to flows

The mac-spoofing filtering done by iptables was
not adequate. See the bug report and change
I39dc0e23fc118ede19ef2d986b29fc5a8e48ff78 for
more information.

This patch adds flows to the OVS agent to block
any traffic from the VM that isn't in the allowed
address pairs macs or the mac address field of
the port.

Closes-Bug: #1558658
Change-Id: I02984b21872e0f183db7404c10d8180dbd89075f
This commit is contained in:
Kevin Benton 2016-03-25 04:47:28 -07:00
parent be298f8bc3
commit 997d7b03fb
7 changed files with 93 additions and 17 deletions

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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)

View File

@ -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,

View File

@ -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,