Merge "Add ARP spoofing protection for LinuxBridge agent"

This commit is contained in:
Jenkins 2015-07-08 20:40:08 +00:00 committed by Gerrit Code Review
commit 4d3d58dc11
7 changed files with 264 additions and 5 deletions

View File

@ -0,0 +1,128 @@
# 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_concurrency import lockutils
from oslo_log import log as logging
from neutron.agent.linux import ip_lib
from neutron.i18n import _LI
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(_LI("Skipping ARP spoofing rules for port '%s' because "
"it has port security disabled"), vif)
return
# collect all of the addresses and cidrs that belong to the port
addresses = {f['ip_address'] for f in port_details['fixed_ips']}
if port_details.get('allowed_address_pairs'):
addresses |= {p['ip_address']
for p in port_details['allowed_address_pairs']}
addresses = {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(_LI("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(NAMESPACE).netns.execute
return execute(['ebtables'] + comm, run_as_root=True)

View File

@ -53,6 +53,22 @@ agent_opts = [
help=_("Set new timeout in seconds for new rpc calls after "
"agent receives SIGTERM. If value is set to 0, rpc "
"timeout won't be changed")),
# TODO(kevinbenton): The following opt is duplicated between the OVS agent
# and the Linuxbridge agent to make it easy to back-port. These shared opts
# should be moved into a common agent config options location as part of
# the deduplication work.
cfg.BoolOpt('prevent_arp_spoofing', default=True,
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. For OVS, it "
"requires a version that supports matching ARP "
"headers."))
]

View File

@ -47,6 +47,7 @@ from neutron.i18n import _LE, _LI, _LW
from neutron.plugins.common import constants as p_const
from neutron.plugins.ml2.drivers.l2pop.rpc_manager \
import l2population_rpc as l2pop_rpc
from neutron.plugins.ml2.drivers.linuxbridge.agent import arp_protect
from neutron.plugins.ml2.drivers.linuxbridge.agent.common import config # noqa
from neutron.plugins.ml2.drivers.linuxbridge.agent.common \
import constants as lconst
@ -768,6 +769,7 @@ class LinuxBridgeNeutronAgentRPC(service.Service):
self.quitting_rpc_timeout = quitting_rpc_timeout
def start(self):
self.prevent_arp_spoofing = cfg.CONF.AGENT.prevent_arp_spoofing
self.setup_linux_bridge(self.interface_mappings)
configurations = {'interface_mappings': self.interface_mappings}
if self.br_mgr.vxlan_mode != lconst.VXLAN_NONE:
@ -895,6 +897,11 @@ class LinuxBridgeNeutronAgentRPC(service.Service):
if 'port_id' in device_details:
LOG.info(_LI("Port %(device)s updated. Details: %(details)s"),
{'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']:
# create the networking for the port
network_type = device_details.get('network_type')
@ -948,6 +955,8 @@ class LinuxBridgeNeutronAgentRPC(service.Service):
LOG.info(_LI("Port %s updated."), device)
else:
LOG.debug("Device %s not defined on plugin", device)
if self.prevent_arp_spoofing:
arp_protect.delete_arp_spoofing_protection(devices)
return resync
def scan_devices(self, previous, sync):
@ -968,6 +977,10 @@ class LinuxBridgeNeutronAgentRPC(service.Service):
'current': set(),
'updated': 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:
# This is the first iteration, or the previous one had a problem.

View File

@ -86,8 +86,9 @@ agent_opts = [
"(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. "
"This requires a version of OVS that supports matching "
"ARP headers.")),
"For LinuxBridge, this requires ebtables. For OVS, it "
"requires a version that supports matching ARP "
"headers.")),
cfg.BoolOpt('dont_fragment', default=True,
help=_("Set or un-set the don't fragment (DF) bit on "
"outgoing IP packet carrying GRE/VXLAN tunnel.")),

View File

@ -76,19 +76,19 @@ class PeerMachines(fixtures.Fixture):
:type machines: FakeMachine list
"""
AMOUNT = 2
CIDR = '192.168.0.1/24'
def __init__(self, bridge, ip_cidr=None, gateway_ip=None):
def __init__(self, bridge, ip_cidr=None, gateway_ip=None, amount=2):
super(PeerMachines, self).__init__()
self.bridge = bridge
self.ip_cidr = ip_cidr or self.CIDR
self.gateway_ip = gateway_ip
self.amount = amount
def _setUp(self):
self.machines = []
for index in range(self.AMOUNT):
for index in range(self.amount):
ip_cidr = net_helpers.increment_ip_cidr(self.ip_cidr, index)
self.machines.append(
self.useFixture(

View File

@ -0,0 +1,100 @@
# 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.
from neutron.plugins.ml2.drivers.linuxbridge.agent import arp_protect
from neutron.tests.common import machine_fixtures
from neutron.tests.common import net_helpers
from neutron.tests.functional import base as functional_base
no_arping = net_helpers.assert_no_arping
arping = net_helpers.assert_arping
class LinuxBridgeARPSpoofTestCase(functional_base.BaseSudoTestCase):
def setUp(self):
super(LinuxBridgeARPSpoofTestCase, self).setUp()
lbfixture = self.useFixture(net_helpers.LinuxBridgeFixture())
self.addCleanup(setattr, arp_protect, 'NAMESPACE', None)
arp_protect.NAMESPACE = lbfixture.namespace
bridge = lbfixture.bridge
self.source, self.destination, self.observer = self.useFixture(
machine_fixtures.PeerMachines(bridge, amount=3)).machines
def _add_arp_protection(self, machine, addresses, extra_port_dict=None):
port_dict = {'fixed_ips': [{'ip_address': a} for a in addresses]}
if extra_port_dict:
port_dict.update(extra_port_dict)
name = net_helpers.VethFixture.get_peer_name(machine.port.name)
arp_protect.setup_arp_spoofing_protection(name, port_dict)
self.addCleanup(arp_protect.delete_arp_spoofing_protection,
[name])
def test_arp_no_protection(self):
arping(self.source.namespace, self.destination.ip)
arping(self.destination.namespace, self.source.ip)
def test_arp_correct_protection(self):
self._add_arp_protection(self.source, [self.source.ip])
self._add_arp_protection(self.destination, [self.destination.ip])
arping(self.source.namespace, self.destination.ip)
arping(self.destination.namespace, self.source.ip)
def test_arp_fails_incorrect_protection(self):
self._add_arp_protection(self.source, ['1.1.1.1'])
self._add_arp_protection(self.destination, ['2.2.2.2'])
no_arping(self.source.namespace, self.destination.ip)
no_arping(self.destination.namespace, self.source.ip)
def test_arp_protection_removal(self):
self._add_arp_protection(self.source, ['1.1.1.1'])
self._add_arp_protection(self.destination, ['2.2.2.2'])
no_arping(self.observer.namespace, self.destination.ip)
no_arping(self.observer.namespace, self.source.ip)
name = net_helpers.VethFixture.get_peer_name(self.source.port.name)
arp_protect.delete_arp_spoofing_protection([name])
# spoofing should have been removed from source, but not dest
arping(self.observer.namespace, self.source.ip)
no_arping(self.observer.namespace, self.destination.ip)
def test_arp_protection_update(self):
self._add_arp_protection(self.source, ['1.1.1.1'])
self._add_arp_protection(self.destination, ['2.2.2.2'])
no_arping(self.observer.namespace, self.destination.ip)
no_arping(self.observer.namespace, self.source.ip)
self._add_arp_protection(self.source, ['192.0.0.0/1'])
# spoofing should have been updated on source, but not dest
arping(self.observer.namespace, self.source.ip)
no_arping(self.observer.namespace, self.destination.ip)
def test_arp_protection_port_security_disabled(self):
self._add_arp_protection(self.source, ['1.1.1.1'])
no_arping(self.observer.namespace, self.source.ip)
self._add_arp_protection(self.source, ['1.1.1.1'],
{'port_security_enabled': False})
arping(self.observer.namespace, self.source.ip)
def test_arp_protection_dead_reference_removal(self):
self._add_arp_protection(self.source, ['1.1.1.1'])
self._add_arp_protection(self.destination, ['2.2.2.2'])
no_arping(self.observer.namespace, self.destination.ip)
no_arping(self.observer.namespace, self.source.ip)
name = net_helpers.VethFixture.get_peer_name(self.source.port.name)
# This should remove all arp protect rules that aren't source port
arp_protect.delete_unreferenced_arp_protection([name])
no_arping(self.observer.namespace, self.source.ip)
arping(self.observer.namespace, self.destination.ip)

View File

@ -89,6 +89,7 @@ class TestLinuxBridgeAgent(base.BaseTestCase):
super(TestLinuxBridgeAgent, self).setUp()
# disable setting up periodic state reporting
cfg.CONF.set_override('report_interval', 0, 'AGENT')
cfg.CONF.set_override('prevent_arp_spoofing', False, 'AGENT')
cfg.CONF.set_default('firewall_driver',
'neutron.agent.firewall.NoopFirewallDriver',
group='SECURITYGROUP')