Add ARP spoofing protection for LinuxBridge agent
This is a backport for the fix that went into master to address this bug. This patch adds ARP spoofing protection for the Linux Bridge agent based on ebtables. The protection is enabled and disabled with the same 'prevent_arp_spoofing' agent config flag added for the OVS agent in I7c079b779245a0af6bc793564fa8a560e4226afe. The protection works by setting up an ebtables chain for each port and jumping all ARP traffic to that chain. The port-specific chains have a default DROP policy and then have allow rules installed that only allow ARP traffic with a source CIDR that matches one of the port's fixed IPs or an allowed address pair. Change-Id: I0b0e3b1272472385dff060897ecbd25e93fd78e7 Closes-Bug: #1274034
This commit is contained in:
parent
429d2e1ecf
commit
1b73fbd705
|
@ -0,0 +1,13 @@
|
||||||
|
# neutron-rootwrap command filters for nodes on which neutron is
|
||||||
|
# expected to control network
|
||||||
|
#
|
||||||
|
# This file should be owned by (and only-writeable by) the root user
|
||||||
|
|
||||||
|
# format seems to be
|
||||||
|
# cmd-name: filter-name, raw-command, user, args
|
||||||
|
|
||||||
|
[Filters]
|
||||||
|
|
||||||
|
# neutron/agent/linux/ebtables_driver.py
|
||||||
|
ebtables: CommandFilter, ebtables, root
|
||||||
|
ebtablesEnv: EnvFilter, ebtables, root, EBTABLES_ATOMIC_FILE=
|
|
@ -89,3 +89,14 @@ def arp_responder_supported(root_helper):
|
||||||
dl_vlan=42,
|
dl_vlan=42,
|
||||||
nw_dst='%s' % ip,
|
nw_dst='%s' % ip,
|
||||||
actions=actions)
|
actions=actions)
|
||||||
|
|
||||||
|
|
||||||
|
def ebtables_supported():
|
||||||
|
try:
|
||||||
|
cmd = ['ebtables', '--version']
|
||||||
|
agent_utils.execute(cmd)
|
||||||
|
return True
|
||||||
|
except (OSError, RuntimeError, IndexError, ValueError) as e:
|
||||||
|
LOG.debug("Exception while checking for installed ebtables. "
|
||||||
|
"Exception: %s", e)
|
||||||
|
return False
|
|
@ -71,6 +71,14 @@ def check_arp_responder():
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def check_ebtables():
|
||||||
|
result = checks.ebtables_supported()
|
||||||
|
if not result:
|
||||||
|
LOG.error(_LE('Cannot run ebtables. Please ensure that it '
|
||||||
|
'is installed.'))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
# Define CLI opts to test specific features, with a calback for the test
|
# Define CLI opts to test specific features, with a calback for the test
|
||||||
OPTS = [
|
OPTS = [
|
||||||
BoolOptCallback('ovs_vxlan', check_ovs_vxlan, default=False,
|
BoolOptCallback('ovs_vxlan', check_ovs_vxlan, default=False,
|
||||||
|
@ -81,6 +89,8 @@ OPTS = [
|
||||||
help=_('Check for nova notification support')),
|
help=_('Check for nova notification support')),
|
||||||
BoolOptCallback('arp_responder', check_arp_responder, default=False,
|
BoolOptCallback('arp_responder', check_arp_responder, default=False,
|
||||||
help=_('Check for ARP responder support')),
|
help=_('Check for ARP responder support')),
|
||||||
|
BoolOptCallback('ebtables_installed', check_ebtables, default=False,
|
||||||
|
help=_('Check ebtables installation')),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -101,6 +111,8 @@ def enable_tests_from_config():
|
||||||
cfg.CONF.set_override('nova_notify', True)
|
cfg.CONF.set_override('nova_notify', True)
|
||||||
if cfg.CONF.AGENT.arp_responder:
|
if cfg.CONF.AGENT.arp_responder:
|
||||||
cfg.CONF.set_override('arp_responder', True)
|
cfg.CONF.set_override('arp_responder', True)
|
||||||
|
if cfg.CONF.AGENT.prevent_arp_spoofing:
|
||||||
|
cfg.CONF.set_override('ebtables_installed', True)
|
||||||
|
|
||||||
|
|
||||||
def all_tests_passed():
|
def all_tests_passed():
|
||||||
|
|
|
@ -0,0 +1,136 @@
|
||||||
|
# Copyright (c) 2015 Mirantis, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import netaddr
|
||||||
|
|
||||||
|
from oslo.config import cfg
|
||||||
|
|
||||||
|
from neutron.agent.linux import ip_lib
|
||||||
|
from neutron.openstack.common import lockutils
|
||||||
|
from neutron.openstack.common import log as logging
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
SPOOF_CHAIN_PREFIX = 'neutronARP-'
|
||||||
|
|
||||||
|
|
||||||
|
def setup_arp_spoofing_protection(vif, port_details):
|
||||||
|
current_rules = ebtables(['-L']).splitlines()
|
||||||
|
if not port_details.get('port_security_enabled', True):
|
||||||
|
# clear any previous entries related to this port
|
||||||
|
delete_arp_spoofing_protection([vif], current_rules)
|
||||||
|
LOG.info(_("Skipping ARP spoofing rules for port '%s' because "
|
||||||
|
"it has port security disabled"), vif)
|
||||||
|
return
|
||||||
|
if port_details['device_owner'].startswith('network:'):
|
||||||
|
# clear any previous entries related to this port
|
||||||
|
delete_arp_spoofing_protection([vif], current_rules)
|
||||||
|
LOG.debug("Skipping ARP spoofing rules for network owned port "
|
||||||
|
"'%s'.", vif)
|
||||||
|
return
|
||||||
|
# collect all of the addresses and cidrs that belong to the port
|
||||||
|
addresses = set(f['ip_address'] for f in port_details['fixed_ips'])
|
||||||
|
if port_details.get('allowed_address_pairs'):
|
||||||
|
addresses |= set(p['ip_address']
|
||||||
|
for p in port_details['allowed_address_pairs'])
|
||||||
|
|
||||||
|
addresses = set(ip for ip in addresses
|
||||||
|
if netaddr.IPNetwork(ip).version == 4)
|
||||||
|
if any(netaddr.IPNetwork(ip).prefixlen == 0 for ip in addresses):
|
||||||
|
# don't try to install protection because a /0 prefix allows any
|
||||||
|
# address anyway and the ARP_SPA can only match on /1 or more.
|
||||||
|
return
|
||||||
|
|
||||||
|
install_arp_spoofing_protection(vif, addresses, current_rules)
|
||||||
|
|
||||||
|
|
||||||
|
def chain_name(vif):
|
||||||
|
# start each chain with a common identifer for cleanup to find
|
||||||
|
return '%s%s' % (SPOOF_CHAIN_PREFIX, vif)
|
||||||
|
|
||||||
|
|
||||||
|
@lockutils.synchronized('ebtables')
|
||||||
|
def delete_arp_spoofing_protection(vifs, current_rules=None):
|
||||||
|
if not current_rules:
|
||||||
|
current_rules = ebtables(['-L']).splitlines()
|
||||||
|
# delete the jump rule and then delete the whole chain
|
||||||
|
jumps = [vif for vif in vifs if vif_jump_present(vif, current_rules)]
|
||||||
|
for vif in jumps:
|
||||||
|
ebtables(['-D', 'FORWARD', '-i', vif, '-j',
|
||||||
|
chain_name(vif), '-p', 'ARP'])
|
||||||
|
for vif in vifs:
|
||||||
|
if chain_exists(chain_name(vif), current_rules):
|
||||||
|
ebtables(['-X', chain_name(vif)])
|
||||||
|
|
||||||
|
|
||||||
|
def delete_unreferenced_arp_protection(current_vifs):
|
||||||
|
# deletes all jump rules and chains that aren't in current_vifs but match
|
||||||
|
# the spoof prefix
|
||||||
|
output = ebtables(['-L']).splitlines()
|
||||||
|
to_delete = []
|
||||||
|
for line in output:
|
||||||
|
# we're looking to find and turn the following:
|
||||||
|
# Bridge chain: SPOOF_CHAIN_PREFIXtap199, entries: 0, policy: DROP
|
||||||
|
# into 'tap199'
|
||||||
|
if line.startswith('Bridge chain: %s' % SPOOF_CHAIN_PREFIX):
|
||||||
|
devname = line.split(SPOOF_CHAIN_PREFIX, 1)[1].split(',')[0]
|
||||||
|
if devname not in current_vifs:
|
||||||
|
to_delete.append(devname)
|
||||||
|
LOG.info(_("Clearing orphaned ARP spoofing entries for devices %s"),
|
||||||
|
to_delete)
|
||||||
|
delete_arp_spoofing_protection(to_delete, output)
|
||||||
|
|
||||||
|
|
||||||
|
@lockutils.synchronized('ebtables')
|
||||||
|
def install_arp_spoofing_protection(vif, addresses, current_rules):
|
||||||
|
# make a VIF-specific ARP chain so we don't conflict with other rules
|
||||||
|
vif_chain = chain_name(vif)
|
||||||
|
if not chain_exists(vif_chain, current_rules):
|
||||||
|
ebtables(['-N', vif_chain, '-P', 'DROP'])
|
||||||
|
# flush the chain to clear previous accepts. this will cause dropped ARP
|
||||||
|
# packets until the allows are installed, but that's better than leaked
|
||||||
|
# spoofed packets and ARP can handle losses.
|
||||||
|
ebtables(['-F', vif_chain])
|
||||||
|
for addr in addresses:
|
||||||
|
ebtables(['-A', vif_chain, '-p', 'ARP', '--arp-ip-src', addr,
|
||||||
|
'-j', 'ACCEPT'])
|
||||||
|
# check if jump rule already exists, if not, install it
|
||||||
|
if not vif_jump_present(vif, current_rules):
|
||||||
|
ebtables(['-A', 'FORWARD', '-i', vif, '-j',
|
||||||
|
vif_chain, '-p', 'ARP'])
|
||||||
|
|
||||||
|
|
||||||
|
def chain_exists(chain, current_rules):
|
||||||
|
for rule in current_rules:
|
||||||
|
if rule.startswith('Bridge chain: %s' % chain):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def vif_jump_present(vif, current_rules):
|
||||||
|
searches = (('-i %s' % vif), ('-j %s' % chain_name(vif)), ('-p ARP'))
|
||||||
|
for line in current_rules:
|
||||||
|
if all(s in line for s in searches):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# Used to scope ebtables commands in testing
|
||||||
|
NAMESPACE = None
|
||||||
|
|
||||||
|
|
||||||
|
def ebtables(comm):
|
||||||
|
execute = ip_lib.IPWrapper(root_helper=cfg.CONF.AGENT.root_helper,
|
||||||
|
namespace=NAMESPACE).netns.execute
|
||||||
|
return execute(['ebtables'] + comm)
|
|
@ -44,6 +44,7 @@ from neutron import context
|
||||||
from neutron.openstack.common import log as logging
|
from neutron.openstack.common import log as logging
|
||||||
from neutron.openstack.common import loopingcall
|
from neutron.openstack.common import loopingcall
|
||||||
from neutron.plugins.common import constants as p_const
|
from neutron.plugins.common import constants as p_const
|
||||||
|
from neutron.plugins.linuxbridge.agent import arp_protect
|
||||||
from neutron.plugins.linuxbridge.common import config # noqa
|
from neutron.plugins.linuxbridge.common import config # noqa
|
||||||
from neutron.plugins.linuxbridge.common import constants as lconst
|
from neutron.plugins.linuxbridge.common import constants as lconst
|
||||||
|
|
||||||
|
@ -767,6 +768,7 @@ class LinuxBridgeNeutronAgentRPC(sg_rpc.SecurityGroupAgentRpcMixin):
|
||||||
root_helper):
|
root_helper):
|
||||||
self.polling_interval = polling_interval
|
self.polling_interval = polling_interval
|
||||||
self.root_helper = root_helper
|
self.root_helper = root_helper
|
||||||
|
self.prevent_arp_spoofing = cfg.CONF.AGENT.prevent_arp_spoofing
|
||||||
self.setup_linux_bridge(interface_mappings)
|
self.setup_linux_bridge(interface_mappings)
|
||||||
configurations = {'interface_mappings': interface_mappings}
|
configurations = {'interface_mappings': interface_mappings}
|
||||||
if self.br_mgr.vxlan_mode != lconst.VXLAN_NONE:
|
if self.br_mgr.vxlan_mode != lconst.VXLAN_NONE:
|
||||||
|
@ -881,6 +883,11 @@ class LinuxBridgeNeutronAgentRPC(sg_rpc.SecurityGroupAgentRpcMixin):
|
||||||
if 'port_id' in device_details:
|
if 'port_id' in device_details:
|
||||||
LOG.info(_("Port %(device)s updated. Details: %(details)s"),
|
LOG.info(_("Port %(device)s updated. Details: %(details)s"),
|
||||||
{'device': device, 'details': device_details})
|
{'device': device, 'details': device_details})
|
||||||
|
if self.prevent_arp_spoofing:
|
||||||
|
port = self.br_mgr.get_tap_device_name(
|
||||||
|
device_details['port_id'])
|
||||||
|
arp_protect.setup_arp_spoofing_protection(port,
|
||||||
|
device_details)
|
||||||
if device_details['admin_state_up']:
|
if device_details['admin_state_up']:
|
||||||
# create the networking for the port
|
# create the networking for the port
|
||||||
network_type = device_details.get('network_type')
|
network_type = device_details.get('network_type')
|
||||||
|
@ -934,6 +941,9 @@ class LinuxBridgeNeutronAgentRPC(sg_rpc.SecurityGroupAgentRpcMixin):
|
||||||
LOG.info(_("Port %s updated."), device)
|
LOG.info(_("Port %s updated."), device)
|
||||||
else:
|
else:
|
||||||
LOG.debug(_("Device %s not defined on plugin"), device)
|
LOG.debug(_("Device %s not defined on plugin"), device)
|
||||||
|
self.br_mgr.remove_empty_bridges()
|
||||||
|
if self.prevent_arp_spoofing:
|
||||||
|
arp_protect.delete_arp_spoofing_protection(devices)
|
||||||
return resync
|
return resync
|
||||||
|
|
||||||
def scan_devices(self, previous, sync):
|
def scan_devices(self, previous, sync):
|
||||||
|
@ -954,6 +964,10 @@ class LinuxBridgeNeutronAgentRPC(sg_rpc.SecurityGroupAgentRpcMixin):
|
||||||
'current': set(),
|
'current': set(),
|
||||||
'updated': set(),
|
'updated': set(),
|
||||||
'removed': set()}
|
'removed': set()}
|
||||||
|
# clear any orphaned ARP spoofing rules (e.g. interface was
|
||||||
|
# manually deleted)
|
||||||
|
if self.prevent_arp_spoofing:
|
||||||
|
arp_protect.delete_unreferenced_arp_protection(current_devices)
|
||||||
|
|
||||||
if sync:
|
if sync:
|
||||||
# This is the first iteration, or the previous one had a problem.
|
# This is the first iteration, or the previous one had a problem.
|
||||||
|
|
|
@ -62,6 +62,16 @@ agent_opts = [
|
||||||
"polling for local device changes.")),
|
"polling for local device changes.")),
|
||||||
cfg.BoolOpt('rpc_support_old_agents', default=False,
|
cfg.BoolOpt('rpc_support_old_agents', default=False,
|
||||||
help=_("Enable server RPC compatibility with old agents")),
|
help=_("Enable server RPC compatibility with old agents")),
|
||||||
|
cfg.BoolOpt('prevent_arp_spoofing', default=False,
|
||||||
|
help=_("Enable suppression of ARP responses that don't match "
|
||||||
|
"an IP address that belongs to the port from which "
|
||||||
|
"they originate. Note: This prevents the VMs attached "
|
||||||
|
"to this agent from spoofing, it doesn't protect them "
|
||||||
|
"from other devices which have the capability to spoof "
|
||||||
|
"(e.g. bare metal or VMs attached to agents without "
|
||||||
|
"this flag set to True). Spoofing rules will not be "
|
||||||
|
"added to any ports that have port security disabled. "
|
||||||
|
"For LinuxBridge, this requires ebtables.")),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ from neutron.common import rpc as n_rpc
|
||||||
from neutron.common import topics
|
from neutron.common import topics
|
||||||
from neutron.common import utils
|
from neutron.common import utils
|
||||||
from neutron.extensions import portbindings
|
from neutron.extensions import portbindings
|
||||||
|
from neutron.extensions import portsecurity as psec
|
||||||
from neutron import manager
|
from neutron import manager
|
||||||
from neutron.openstack.common import log
|
from neutron.openstack.common import log
|
||||||
from neutron.plugins.common import constants as service_constants
|
from neutron.plugins.common import constants as service_constants
|
||||||
|
@ -102,6 +103,8 @@ class RpcCallbacks(n_rpc.RpcCallback,
|
||||||
'physical_network': segment[api.PHYSICAL_NETWORK],
|
'physical_network': segment[api.PHYSICAL_NETWORK],
|
||||||
'fixed_ips': port['fixed_ips'],
|
'fixed_ips': port['fixed_ips'],
|
||||||
'device_owner': port['device_owner'],
|
'device_owner': port['device_owner'],
|
||||||
|
'allowed_address_pairs': port['allowed_address_pairs'],
|
||||||
|
'port_security_enabled': port.get(psec.PORTSECURITY, True),
|
||||||
'profile': port[portbindings.PROFILE]}
|
'profile': port[portbindings.PROFILE]}
|
||||||
LOG.debug(_("Returning: %s"), entry)
|
LOG.debug(_("Returning: %s"), entry)
|
||||||
return entry
|
return entry
|
||||||
|
|
|
@ -0,0 +1,298 @@
|
||||||
|
# Copyright (c) 2015 Cisco Systems, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import copy
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from oslo.config import cfg
|
||||||
|
|
||||||
|
from neutron.plugins.linuxbridge.agent import arp_protect
|
||||||
|
from neutron.tests import base
|
||||||
|
|
||||||
|
|
||||||
|
class ArpProtectTestCase(base.BaseTestCase):
|
||||||
|
|
||||||
|
VIF = 'tap3fc5bc14-b1'
|
||||||
|
FIXED_IP = '1.2.3.4'
|
||||||
|
ALLOWED_ADDRESS = '5.6.7.8'
|
||||||
|
CHAIN_NAME = arp_protect.chain_name(VIF)
|
||||||
|
|
||||||
|
EBTABLES_EMPTY_SAMPLE = [
|
||||||
|
'Bridge table: filter',
|
||||||
|
'',
|
||||||
|
'Bridge chain: INPUT, entries: 0, policy: ACCEPT',
|
||||||
|
'',
|
||||||
|
'Bridge chain: FORWARD, entries: 0, policy: ACCEPT',
|
||||||
|
'',
|
||||||
|
'Bridge chain: OUTPUT, entries: 0, policy: ACCEPT',
|
||||||
|
]
|
||||||
|
|
||||||
|
EBTABLES_LOADED_SAMPLE = [
|
||||||
|
'Bridge table: filter',
|
||||||
|
'',
|
||||||
|
'Bridge chain: INPUT, entries: 0, policy: ACCEPT',
|
||||||
|
'',
|
||||||
|
'Bridge chain: FORWARD, entries: 2, policy: ACCEPT',
|
||||||
|
'-p ARP -i %s -j %s' % (VIF, CHAIN_NAME),
|
||||||
|
'',
|
||||||
|
'Bridge chain: OUTPUT, entries: 0, policy: ACCEPT',
|
||||||
|
'',
|
||||||
|
'Bridge chain: %s, entries: 1, policy: DROP' % (CHAIN_NAME),
|
||||||
|
'-p ARP --arp-ip-src %s -j ACCEPT' % (FIXED_IP),
|
||||||
|
]
|
||||||
|
PORT_DETAILS_SAMPLE = {
|
||||||
|
'port_security_enabled': True,
|
||||||
|
'fixed_ips': [{
|
||||||
|
'subnet_id': '12345',
|
||||||
|
'ip_address': FIXED_IP,
|
||||||
|
}],
|
||||||
|
'allowed_address_pairs': [],
|
||||||
|
'device_owner': 'nobody',
|
||||||
|
}
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(ArpProtectTestCase, self).setUp()
|
||||||
|
cfg.CONF.set_override('prevent_arp_spoofing', True, 'AGENT')
|
||||||
|
|
||||||
|
def _do_test_setup_arp_spoofing(self, vif, port_details):
|
||||||
|
with contextlib.nested(
|
||||||
|
mock.patch.object(
|
||||||
|
arp_protect, 'ebtables',
|
||||||
|
return_value='\n'.join(self.EBTABLES_EMPTY_SAMPLE)
|
||||||
|
)
|
||||||
|
) as ebtables_fn: # noqa
|
||||||
|
arp_protect.setup_arp_spoofing_protection(vif, port_details)
|
||||||
|
|
||||||
|
def test_setup_arp_spoofing(self):
|
||||||
|
port_details = copy.deepcopy(self.PORT_DETAILS_SAMPLE)
|
||||||
|
with contextlib.nested(
|
||||||
|
mock.patch.object(arp_protect, 'install_arp_spoofing_protection'),
|
||||||
|
mock.patch.object(arp_protect, 'delete_arp_spoofing_protection'),
|
||||||
|
) as (install_fn, delete_fn):
|
||||||
|
self._do_test_setup_arp_spoofing(self.VIF, port_details)
|
||||||
|
self.assertFalse(delete_fn.called)
|
||||||
|
install_fn.assert_called_once_with(self.VIF,
|
||||||
|
set([self.FIXED_IP]),
|
||||||
|
self.EBTABLES_EMPTY_SAMPLE)
|
||||||
|
|
||||||
|
def test_setup_arp_spoofing_with_allowed_address_pairs(self):
|
||||||
|
port_details = copy.deepcopy(self.PORT_DETAILS_SAMPLE)
|
||||||
|
port_details['allowed_address_pairs'] = [{
|
||||||
|
'mac_address': 'aa:bb:cc:dd:ee:ff',
|
||||||
|
'ip_address': '5.6.7.8',
|
||||||
|
}]
|
||||||
|
with contextlib.nested(
|
||||||
|
mock.patch.object(arp_protect, 'install_arp_spoofing_protection'),
|
||||||
|
mock.patch.object(arp_protect, 'delete_arp_spoofing_protection'),
|
||||||
|
) as (install_fn, delete_fn):
|
||||||
|
self._do_test_setup_arp_spoofing(self.VIF, port_details)
|
||||||
|
self.assertFalse(delete_fn.called)
|
||||||
|
install_fn.assert_called_once_with(self.VIF,
|
||||||
|
set([self.FIXED_IP,
|
||||||
|
self.ALLOWED_ADDRESS]),
|
||||||
|
self.EBTABLES_EMPTY_SAMPLE)
|
||||||
|
|
||||||
|
def test_setup_arp_spoofing_no_pse(self):
|
||||||
|
port_details = copy.deepcopy(self.PORT_DETAILS_SAMPLE)
|
||||||
|
port_details['port_security_enabled'] = False
|
||||||
|
with contextlib.nested(
|
||||||
|
mock.patch.object(arp_protect, 'install_arp_spoofing_protection'),
|
||||||
|
mock.patch.object(arp_protect, 'delete_arp_spoofing_protection'),
|
||||||
|
) as (install_fn, delete_fn):
|
||||||
|
self._do_test_setup_arp_spoofing(self.VIF, port_details)
|
||||||
|
delete_fn.assert_called_once_with([self.VIF],
|
||||||
|
self.EBTABLES_EMPTY_SAMPLE)
|
||||||
|
self.assertFalse(install_fn.called)
|
||||||
|
|
||||||
|
def test_setup_arp_spoofing_network_port(self):
|
||||||
|
port_details = copy.deepcopy(self.PORT_DETAILS_SAMPLE)
|
||||||
|
port_details['device_owner'] = 'network:router_gateway'
|
||||||
|
with contextlib.nested(
|
||||||
|
mock.patch.object(arp_protect, 'install_arp_spoofing_protection'),
|
||||||
|
mock.patch.object(arp_protect, 'delete_arp_spoofing_protection'),
|
||||||
|
) as (install_fn, delete_fn):
|
||||||
|
self._do_test_setup_arp_spoofing(self.VIF, port_details)
|
||||||
|
delete_fn.assert_called_once_with([self.VIF],
|
||||||
|
self.EBTABLES_EMPTY_SAMPLE)
|
||||||
|
self.assertFalse(install_fn.called)
|
||||||
|
|
||||||
|
def test_setup_arp_spoofing_zero_length_prefix(self):
|
||||||
|
port_details = copy.deepcopy(self.PORT_DETAILS_SAMPLE)
|
||||||
|
port_details['allowed_address_pairs'] = [{
|
||||||
|
'mac_address': 'aa:bb:cc:dd:ee:ff',
|
||||||
|
'ip_address': '0.0.0.0/0',
|
||||||
|
}]
|
||||||
|
with contextlib.nested(
|
||||||
|
mock.patch.object(arp_protect, 'install_arp_spoofing_protection'),
|
||||||
|
mock.patch.object(arp_protect, 'delete_arp_spoofing_protection'),
|
||||||
|
) as (install_fn, delete_fn):
|
||||||
|
self._do_test_setup_arp_spoofing(self.VIF, port_details)
|
||||||
|
self.assertFalse(delete_fn.called)
|
||||||
|
self.assertFalse(install_fn.called)
|
||||||
|
|
||||||
|
def test_chain_name(self):
|
||||||
|
name = '%s%s' % (arp_protect.SPOOF_CHAIN_PREFIX, self.VIF)
|
||||||
|
self.assertEqual(name, arp_protect.chain_name(self.VIF))
|
||||||
|
|
||||||
|
def test_delete_arp_spoofing(self):
|
||||||
|
# Note(cfb): We don't call this with contextlib.nested() because
|
||||||
|
# arp_protect.delete_arp_spoofing_protection() has a decorator
|
||||||
|
# which is a non-nested context manager and they don't play nice
|
||||||
|
# together with mock at all.
|
||||||
|
ebtables_p = mock.patch.object(arp_protect, 'ebtables')
|
||||||
|
ebtables = ebtables_p.start()
|
||||||
|
|
||||||
|
arp_protect.delete_arp_spoofing_protection(
|
||||||
|
[self.VIF], current_rules=self.EBTABLES_LOADED_SAMPLE)
|
||||||
|
expected = [
|
||||||
|
mock.call(['-D', 'FORWARD', '-i', self.VIF, '-j',
|
||||||
|
self.CHAIN_NAME, '-p', 'ARP']),
|
||||||
|
mock.call(['-X', self.CHAIN_NAME]),
|
||||||
|
]
|
||||||
|
ebtables.assert_has_calls(expected)
|
||||||
|
|
||||||
|
def test_delete_unreferenced_arp(self):
|
||||||
|
with contextlib.nested(
|
||||||
|
mock.patch.object(
|
||||||
|
arp_protect, 'ebtables',
|
||||||
|
return_value='\n'.join(self.EBTABLES_LOADED_SAMPLE)),
|
||||||
|
mock.patch.object(arp_protect, 'delete_arp_spoofing_protection'),
|
||||||
|
) as (ebtables_fn, delete_fn):
|
||||||
|
arp_protect.delete_unreferenced_arp_protection([])
|
||||||
|
delete_fn.assert_called_once_with([self.VIF],
|
||||||
|
self.EBTABLES_LOADED_SAMPLE)
|
||||||
|
|
||||||
|
def test_install_arp_spoofing_single_ip(self):
|
||||||
|
# Note(cfb): We don't call this with contextlib.nested() because
|
||||||
|
# arp_protect.install.arp_spoofing_protection() has a decorator
|
||||||
|
# which is a non-nested context manager and they don't play nice
|
||||||
|
# together with mock at all.
|
||||||
|
ebtables_p = mock.patch.object(arp_protect, 'ebtables')
|
||||||
|
ebtables = ebtables_p.start()
|
||||||
|
|
||||||
|
arp_protect.install_arp_spoofing_protection(
|
||||||
|
self.VIF, [self.FIXED_IP], self.EBTABLES_EMPTY_SAMPLE)
|
||||||
|
expected = [
|
||||||
|
mock.call(['-N', self.CHAIN_NAME, '-P', 'DROP']),
|
||||||
|
mock.call(['-F', self.CHAIN_NAME]),
|
||||||
|
mock.call(['-A', self.CHAIN_NAME, '-p', 'ARP',
|
||||||
|
'--arp-ip-src', self.FIXED_IP, '-j', 'ACCEPT']),
|
||||||
|
mock.call(['-A', 'FORWARD', '-i', self.VIF, '-j',
|
||||||
|
self.CHAIN_NAME, '-p', 'ARP']),
|
||||||
|
]
|
||||||
|
ebtables.assert_has_calls(expected)
|
||||||
|
|
||||||
|
def test_install_arp_spoofing_multiple_ip(self):
|
||||||
|
# Note(cfb): We don't call this with contextlib.nested() because
|
||||||
|
# arp_protect.install.arp_spoofing_protection() has a decorator
|
||||||
|
# which is a non-nested context manager and they don't play nice
|
||||||
|
# together with mock at all.
|
||||||
|
ebtables_p = mock.patch.object(arp_protect, 'ebtables')
|
||||||
|
ebtables = ebtables_p.start()
|
||||||
|
|
||||||
|
arp_protect.install_arp_spoofing_protection(
|
||||||
|
self.VIF, [self.FIXED_IP, self.ALLOWED_ADDRESS],
|
||||||
|
self.EBTABLES_EMPTY_SAMPLE)
|
||||||
|
expected = [
|
||||||
|
mock.call(['-N', self.CHAIN_NAME, '-P', 'DROP']),
|
||||||
|
mock.call(['-F', self.CHAIN_NAME]),
|
||||||
|
mock.call(['-A', self.CHAIN_NAME, '-p', 'ARP',
|
||||||
|
'--arp-ip-src', self.FIXED_IP, '-j', 'ACCEPT']),
|
||||||
|
mock.call(['-A', self.CHAIN_NAME, '-p', 'ARP',
|
||||||
|
'--arp-ip-src', self.ALLOWED_ADDRESS, '-j', 'ACCEPT']),
|
||||||
|
mock.call(['-A', 'FORWARD', '-i', self.VIF, '-j',
|
||||||
|
self.CHAIN_NAME, '-p', 'ARP']),
|
||||||
|
]
|
||||||
|
ebtables.assert_has_calls(expected)
|
||||||
|
|
||||||
|
def test_install_arp_spoofing_existing_chain(self):
|
||||||
|
# Note(cfb): We don't call this with contextlib.nested() because
|
||||||
|
# arp_protect.install.arp_spoofing_protection() has a decorator
|
||||||
|
# which is a non-nested context manager and they don't play nice
|
||||||
|
# together with mock at all.
|
||||||
|
ebtables_p = mock.patch.object(arp_protect, 'ebtables')
|
||||||
|
ebtables = ebtables_p.start()
|
||||||
|
|
||||||
|
current_rules = [
|
||||||
|
'Bridge table: filter',
|
||||||
|
'',
|
||||||
|
'Bridge chain: INPUT, entries: 0, policy: ACCEPT',
|
||||||
|
'',
|
||||||
|
'Bridge chain: FORWARD, entries: 2, policy: ACCEPT',
|
||||||
|
'',
|
||||||
|
'Bridge chain: OUTPUT, entries: 0, policy: ACCEPT',
|
||||||
|
'',
|
||||||
|
'Bridge chain: %s, entries: 1, policy: DROP' % (self.CHAIN_NAME),
|
||||||
|
]
|
||||||
|
|
||||||
|
arp_protect.install_arp_spoofing_protection(self.VIF,
|
||||||
|
[self.FIXED_IP],
|
||||||
|
current_rules)
|
||||||
|
expected = [
|
||||||
|
mock.call(['-F', self.CHAIN_NAME]),
|
||||||
|
mock.call(['-A', self.CHAIN_NAME, '-p', 'ARP',
|
||||||
|
'--arp-ip-src', self.FIXED_IP, '-j', 'ACCEPT']),
|
||||||
|
mock.call(['-A', 'FORWARD', '-i', self.VIF, '-j',
|
||||||
|
self.CHAIN_NAME, '-p', 'ARP']),
|
||||||
|
]
|
||||||
|
ebtables.assert_has_calls(expected)
|
||||||
|
|
||||||
|
def test_install_arp_spoofing_existing_jump(self):
|
||||||
|
# Note(cfb): We don't call this with contextlib.nested() because
|
||||||
|
# arp_protect.install.arp_spoofing_protection() has a decorator
|
||||||
|
# which is a non-nested context manager and they don't play nice
|
||||||
|
# together with mock at all.
|
||||||
|
ebtables_p = mock.patch.object(arp_protect, 'ebtables')
|
||||||
|
ebtables = ebtables_p.start()
|
||||||
|
|
||||||
|
current_rules = [
|
||||||
|
'Bridge table: filter',
|
||||||
|
'',
|
||||||
|
'Bridge chain: INPUT, entries: 0, policy: ACCEPT',
|
||||||
|
'',
|
||||||
|
'Bridge chain: FORWARD, entries: 2, policy: ACCEPT',
|
||||||
|
'-p ARP -i %s -j %s' % (self.VIF, self.CHAIN_NAME),
|
||||||
|
'',
|
||||||
|
'Bridge chain: OUTPUT, entries: 0, policy: ACCEPT',
|
||||||
|
'',
|
||||||
|
]
|
||||||
|
|
||||||
|
arp_protect.install_arp_spoofing_protection(self.VIF,
|
||||||
|
[self.FIXED_IP],
|
||||||
|
current_rules)
|
||||||
|
expected = [
|
||||||
|
mock.call(['-N', self.CHAIN_NAME, '-P', 'DROP']),
|
||||||
|
mock.call(['-F', self.CHAIN_NAME]),
|
||||||
|
mock.call(['-A', self.CHAIN_NAME, '-p', 'ARP',
|
||||||
|
'--arp-ip-src', self.FIXED_IP, '-j', 'ACCEPT']),
|
||||||
|
]
|
||||||
|
ebtables.assert_has_calls(expected)
|
||||||
|
|
||||||
|
def test_chain_exists(self):
|
||||||
|
self.assertTrue(arp_protect.chain_exists(self.CHAIN_NAME,
|
||||||
|
self.EBTABLES_LOADED_SAMPLE))
|
||||||
|
|
||||||
|
def test_chain_does_not_exist(self):
|
||||||
|
self.assertFalse(arp_protect.chain_exists('foobarbaz',
|
||||||
|
self.EBTABLES_LOADED_SAMPLE))
|
||||||
|
|
||||||
|
def test_vif_jump_present(self):
|
||||||
|
self.assertTrue(arp_protect.vif_jump_present(
|
||||||
|
self.VIF, self.EBTABLES_LOADED_SAMPLE))
|
||||||
|
|
||||||
|
def test_vif_jump_not_present(self):
|
||||||
|
self.assertFalse(arp_protect.vif_jump_present(
|
||||||
|
'foobarbaz', self.EBTABLES_LOADED_SAMPLE))
|
|
@ -38,6 +38,7 @@ data_files =
|
||||||
etc/neutron/rootwrap.d/debug.filters
|
etc/neutron/rootwrap.d/debug.filters
|
||||||
etc/neutron/rootwrap.d/dhcp.filters
|
etc/neutron/rootwrap.d/dhcp.filters
|
||||||
etc/neutron/rootwrap.d/iptables-firewall.filters
|
etc/neutron/rootwrap.d/iptables-firewall.filters
|
||||||
|
etc/neutron/rootwrap.d/ebtables.filters
|
||||||
etc/neutron/rootwrap.d/ipset-firewall.filters
|
etc/neutron/rootwrap.d/ipset-firewall.filters
|
||||||
etc/neutron/rootwrap.d/l3.filters
|
etc/neutron/rootwrap.d/l3.filters
|
||||||
etc/neutron/rootwrap.d/lbaas-haproxy.filters
|
etc/neutron/rootwrap.d/lbaas-haproxy.filters
|
||||||
|
|
Loading…
Reference in New Issue