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:
parent
be298f8bc3
commit
997d7b03fb
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue