Merge "OVS: Add mac spoofing filtering to flows" into stable/mitaka
This commit is contained in:
commit
0c4218202c
|
@ -50,6 +50,9 @@ CANARY_TABLE = 23
|
||||||
# Table for ARP poison/spoofing prevention rules
|
# Table for ARP poison/spoofing prevention rules
|
||||||
ARP_SPOOF_TABLE = 24
|
ARP_SPOOF_TABLE = 24
|
||||||
|
|
||||||
|
# Table for MAC spoof filtering
|
||||||
|
MAC_SPOOF_TABLE = 25
|
||||||
|
|
||||||
# Tables used for ovs firewall
|
# Tables used for ovs firewall
|
||||||
BASE_EGRESS_TABLE = 71
|
BASE_EGRESS_TABLE = 71
|
||||||
RULES_EGRESS_TABLE = 72
|
RULES_EGRESS_TABLE = 72
|
||||||
|
|
|
@ -19,6 +19,8 @@
|
||||||
** OVS agent https://wiki.openstack.org/wiki/Ovs-flow-logic
|
** OVS agent https://wiki.openstack.org/wiki/Ovs-flow-logic
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import netaddr
|
||||||
|
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
from ryu.lib.packet import ether_types
|
from ryu.lib.packet import ether_types
|
||||||
from ryu.lib.packet import icmpv6
|
from ryu.lib.packet import icmpv6
|
||||||
|
@ -174,16 +176,45 @@ class OVSIntegrationBridge(ovs_bridge.OVSAgentBridge):
|
||||||
match=match,
|
match=match,
|
||||||
dest_table_id=constants.ARP_SPOOF_TABLE)
|
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):
|
def install_arp_spoofing_protection(self, port, ip_addresses):
|
||||||
# allow ARP replies as long as they match addresses that actually
|
# allow ARP replies as long as they match addresses that actually
|
||||||
# belong to the port.
|
# belong to the port.
|
||||||
for ip in ip_addresses:
|
for ip in ip_addresses:
|
||||||
masked_ip = self._cidr_to_ryu(ip)
|
masked_ip = self._cidr_to_ryu(ip)
|
||||||
self.install_normal(table_id=constants.ARP_SPOOF_TABLE,
|
self.install_goto(table_id=constants.ARP_SPOOF_TABLE,
|
||||||
priority=2,
|
priority=2,
|
||||||
eth_type=ether_types.ETH_TYPE_ARP,
|
eth_type=ether_types.ETH_TYPE_ARP,
|
||||||
arp_spa=masked_ip,
|
arp_spa=masked_ip,
|
||||||
in_port=port)
|
in_port=port,
|
||||||
|
dest_table_id=constants.MAC_SPOOF_TABLE)
|
||||||
|
|
||||||
# Now that the rules are ready, direct ARP traffic from the port into
|
# Now that the rules are ready, direct ARP traffic from the port into
|
||||||
# the anti-spoof table.
|
# the anti-spoof table.
|
||||||
|
|
|
@ -18,6 +18,9 @@
|
||||||
* references
|
* references
|
||||||
** OVS agent https://wiki.openstack.org/wiki/Ovs-flow-logic
|
** OVS agent https://wiki.openstack.org/wiki/Ovs-flow-logic
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import netaddr
|
||||||
|
|
||||||
from neutron.common import constants as const
|
from neutron.common import constants as const
|
||||||
from neutron.plugins.common import constants as p_const
|
from neutron.plugins.common import constants as p_const
|
||||||
from neutron.plugins.ml2.drivers.openvswitch.agent.common import constants
|
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,
|
icmp_type=const.ICMPV6_TYPE_NA, in_port=port,
|
||||||
actions=("resubmit(,%s)" % constants.ARP_SPOOF_TABLE))
|
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):
|
def install_arp_spoofing_protection(self, port, ip_addresses):
|
||||||
# allow ARPs as long as they match addresses that actually
|
# allow ARPs as long as they match addresses that actually
|
||||||
# belong to the port.
|
# belong to the port.
|
||||||
for ip in ip_addresses:
|
for ip in ip_addresses:
|
||||||
self.install_normal(
|
self.add_flow(
|
||||||
table_id=constants.ARP_SPOOF_TABLE, priority=2,
|
table=constants.ARP_SPOOF_TABLE, priority=2,
|
||||||
proto='arp', arp_spa=ip, in_port=port)
|
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
|
# Now that the rules are ready, direct ARP traffic from the port into
|
||||||
# the anti-spoof table.
|
# the anti-spoof table.
|
||||||
|
|
|
@ -887,12 +887,14 @@ class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin,
|
||||||
LOG.info(_LI("Skipping ARP spoofing rules for port '%s' because "
|
LOG.info(_LI("Skipping ARP spoofing rules for port '%s' because "
|
||||||
"it has port security disabled"), vif.port_name)
|
"it has port security disabled"), vif.port_name)
|
||||||
bridge.delete_arp_spoofing_protection(port=vif.ofport)
|
bridge.delete_arp_spoofing_protection(port=vif.ofport)
|
||||||
|
bridge.set_allowed_macs_for_port(port=vif.ofport, allow_all=True)
|
||||||
return
|
return
|
||||||
if port_details['device_owner'].startswith(
|
if port_details['device_owner'].startswith(
|
||||||
n_const.DEVICE_OWNER_NETWORK_PREFIX):
|
n_const.DEVICE_OWNER_NETWORK_PREFIX):
|
||||||
LOG.debug("Skipping ARP spoofing rules for network owned port "
|
LOG.debug("Skipping ARP spoofing rules for network owned port "
|
||||||
"'%s'.", vif.port_name)
|
"'%s'.", vif.port_name)
|
||||||
bridge.delete_arp_spoofing_protection(port=vif.ofport)
|
bridge.delete_arp_spoofing_protection(port=vif.ofport)
|
||||||
|
bridge.set_allowed_macs_for_port(port=vif.ofport, allow_all=True)
|
||||||
return
|
return
|
||||||
# clear any previous flows related to this port in our ARP table
|
# clear any previous flows related to this port in our ARP table
|
||||||
bridge.delete_arp_spoofing_allow_rules(port=vif.ofport)
|
bridge.delete_arp_spoofing_allow_rules(port=vif.ofport)
|
||||||
|
@ -906,6 +908,7 @@ class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin,
|
||||||
for p in port_details['allowed_address_pairs']
|
for p in port_details['allowed_address_pairs']
|
||||||
if p.get('mac_address')}
|
if p.get('mac_address')}
|
||||||
|
|
||||||
|
bridge.set_allowed_macs_for_port(vif.ofport, mac_addresses)
|
||||||
ipv6_addresses = {ip for ip in addresses
|
ipv6_addresses = {ip for ip in addresses
|
||||||
if netaddr.IPNetwork(ip).version == 6}
|
if netaddr.IPNetwork(ip).version == 6}
|
||||||
# Allow neighbor advertisements for LLA address.
|
# Allow neighbor advertisements for LLA address.
|
||||||
|
@ -1181,6 +1184,7 @@ class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin,
|
||||||
ofports_deleted = set(previous.values()) - set(current.values())
|
ofports_deleted = set(previous.values()) - set(current.values())
|
||||||
for ofport in ofports_deleted:
|
for ofport in ofports_deleted:
|
||||||
self.int_br.delete_arp_spoofing_protection(port=ofport)
|
self.int_br.delete_arp_spoofing_protection(port=ofport)
|
||||||
|
self.int_br.set_allowed_macs_for_port(port=ofport, allow_all=True)
|
||||||
|
|
||||||
# store map for next iteration
|
# store map for next iteration
|
||||||
self.vifname_to_ofport_map = current
|
self.vifname_to_ofport_map = current
|
||||||
|
|
|
@ -161,6 +161,17 @@ class ARPSpoofTestCase(OVSAgentTestBase):
|
||||||
self.dst_p.addr.add('%s/24' % self.dst_addr)
|
self.dst_p.addr.add('%s/24' % self.dst_addr)
|
||||||
net_helpers.assert_ping(self.src_namespace, self.dst_addr, count=2)
|
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):
|
def test_arp_spoof_doesnt_block_ipv6(self):
|
||||||
self.src_addr = '2000::1'
|
self.src_addr = '2000::1'
|
||||||
self.dst_addr = '2000::2'
|
self.dst_addr = '2000::2'
|
||||||
|
@ -259,7 +270,7 @@ class ARPSpoofTestCase(OVSAgentTestBase):
|
||||||
net_helpers.assert_ping(self.src_namespace, self.dst_addr, count=2)
|
net_helpers.assert_ping(self.src_namespace, self.dst_addr, count=2)
|
||||||
|
|
||||||
def _setup_arp_spoof_for_port(self, port, addrs, psec=True,
|
def _setup_arp_spoof_for_port(self, port, addrs, psec=True,
|
||||||
device_owner='nobody'):
|
device_owner='nobody', mac=None):
|
||||||
vif = next(
|
vif = next(
|
||||||
vif for vif in self.br.get_vif_ports() if vif.port_name == port)
|
vif for vif in self.br.get_vif_ports() if vif.port_name == port)
|
||||||
ip_addr = addrs.pop()
|
ip_addr = addrs.pop()
|
||||||
|
@ -268,6 +279,8 @@ class ARPSpoofTestCase(OVSAgentTestBase):
|
||||||
'device_owner': device_owner,
|
'device_owner': device_owner,
|
||||||
'allowed_address_pairs': [
|
'allowed_address_pairs': [
|
||||||
dict(ip_address=ip) for ip in addrs]}
|
dict(ip_address=ip) for ip in addrs]}
|
||||||
|
if mac:
|
||||||
|
vif.vif_mac = mac
|
||||||
ovsagt.OVSNeutronAgent.setup_arp_spoofing_protection(
|
ovsagt.OVSNeutronAgent.setup_arp_spoofing_protection(
|
||||||
self.br_int, vif, details)
|
self.br_int, vif, details)
|
||||||
|
|
||||||
|
|
|
@ -347,9 +347,7 @@ class OVSIntegrationBridgeTest(ovs_bridge_test_base.OVSBridgeTestBase):
|
||||||
call._send_msg(ofpp.OFPFlowMod(dp,
|
call._send_msg(ofpp.OFPFlowMod(dp,
|
||||||
cookie=self.stamp,
|
cookie=self.stamp,
|
||||||
instructions=[
|
instructions=[
|
||||||
ofpp.OFPInstructionActions(ofp.OFPIT_APPLY_ACTIONS, [
|
ofpp.OFPInstructionGotoTable(table_id=25),
|
||||||
ofpp.OFPActionOutput(ofp.OFPP_NORMAL, 0),
|
|
||||||
]),
|
|
||||||
],
|
],
|
||||||
match=ofpp.OFPMatch(
|
match=ofpp.OFPMatch(
|
||||||
eth_type=self.ether_types.ETH_TYPE_ARP,
|
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,
|
call._send_msg(ofpp.OFPFlowMod(dp,
|
||||||
cookie=self.stamp,
|
cookie=self.stamp,
|
||||||
instructions=[
|
instructions=[
|
||||||
ofpp.OFPInstructionActions(ofp.OFPIT_APPLY_ACTIONS, [
|
ofpp.OFPInstructionGotoTable(table_id=25),
|
||||||
ofpp.OFPActionOutput(ofp.OFPP_NORMAL, 0),
|
|
||||||
]),
|
|
||||||
],
|
],
|
||||||
match=ofpp.OFPMatch(
|
match=ofpp.OFPMatch(
|
||||||
eth_type=self.ether_types.ETH_TYPE_ARP,
|
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']
|
ip_addresses = ['192.0.2.1', '192.0.2.2/32']
|
||||||
self.br.install_arp_spoofing_protection(port, ip_addresses)
|
self.br.install_arp_spoofing_protection(port, ip_addresses)
|
||||||
expected = [
|
expected = [
|
||||||
call.add_flow(proto='arp', actions='normal',
|
call.add_flow(proto='arp', actions='resubmit(,25)',
|
||||||
arp_spa='192.0.2.1',
|
arp_spa='192.0.2.1',
|
||||||
priority=2, table=24, in_port=8888),
|
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',
|
arp_spa='192.0.2.2/32',
|
||||||
priority=2, table=24, in_port=8888),
|
priority=2, table=24, in_port=8888),
|
||||||
call.add_flow(priority=10, table=0, in_port=8888,
|
call.add_flow(priority=10, table=0, in_port=8888,
|
||||||
|
|
Loading…
Reference in New Issue