Linux Bridge: Add mac spoofing filtering to ebtables

The current mac-spoofing code in iptables has two issues.
First, it occurs after the address discovery allow rules
(e.g. DHCP), so MAC addresses can be spoofed on discovery
protocols. Second, since it is based on iptables, it
doesn't apply to protocols like STP.

This means a VM could generate one of these types of packets
with a spoofed MAC address to trick switches into learning
that the spoofed MAC now belongs to the VM's port. The impact
of this depends on the configuration of the environment
(e.g. use of L2pop: see the bug report for details).

This patch adds MAC spoofing filtering to the ARP protection
code for Linux bridge based on ebtables. Only traffic sourced
from the MAC address on the port or in the allowed address
pair MACs will be allowed.

This filtering will not be enabled if the port has port
security disabled or if the device_owner starts with 'network:'.

Change-Id: I39dc0e23fc118ede19ef2d986b29fc5a8e48ff78
Partial-Bug: #1558658
This commit is contained in:
Kevin Benton 2016-03-25 02:45:11 -07:00
parent ab614a10a7
commit be298f8bc3
2 changed files with 94 additions and 1 deletions

View File

@ -23,6 +23,7 @@ from neutron.common import utils
LOG = logging.getLogger(__name__)
SPOOF_CHAIN_PREFIX = 'neutronARP-'
MAC_CHAIN_PREFIX = 'neutronMAC-'
def setup_arp_spoofing_protection(vif, port_details):
@ -39,6 +40,7 @@ def setup_arp_spoofing_protection(vif, port_details):
LOG.debug("Skipping ARP spoofing rules for network owned port "
"'%s'.", vif)
return
_install_mac_spoofing_protection(vif, port_details, current_rules)
# 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'):
@ -72,6 +74,7 @@ def delete_arp_spoofing_protection(vifs, current_rules=None):
for vif in vifs:
if chain_exists(chain_name(vif), current_rules):
ebtables(['-X', chain_name(vif)])
_delete_mac_spoofing_protection(vifs, current_rules)
def delete_unreferenced_arp_protection(current_vifs):
@ -126,6 +129,62 @@ def vif_jump_present(vif, current_rules):
return False
@lockutils.synchronized('ebtables')
def _install_mac_spoofing_protection(vif, port_details, current_rules):
mac_addresses = {port_details['mac_address']}
if port_details.get('allowed_address_pairs'):
mac_addresses |= {p['mac_address']
for p in port_details['allowed_address_pairs']}
mac_addresses = list(mac_addresses)
vif_chain = _mac_chain_name(vif)
# mac filter chain for each vif which has a default deny
if not chain_exists(vif_chain, current_rules):
ebtables(['-N', vif_chain, '-P', 'DROP'])
# check if jump rule already exists, if not, install it
if not _mac_vif_jump_present(vif, current_rules):
ebtables(['-A', 'FORWARD', '-i', vif, '-j', vif_chain])
# we can't just feed all allowed macs at once because we can exceed
# the maximum argument size. limit to 500 per rule.
for chunk in (mac_addresses[i:i + 500]
for i in range(0, len(mac_addresses), 500)):
new_rule = ['-A', vif_chain, '-i', vif,
'--among-src', ','.join(chunk), '-j', 'RETURN']
ebtables(new_rule)
_delete_vif_mac_rules(vif, current_rules)
def _mac_vif_jump_present(vif, current_rules):
searches = (('-i %s' % vif), ('-j %s' % _mac_chain_name(vif)))
for line in current_rules:
if all(s in line for s in searches):
return True
return False
def _mac_chain_name(vif):
return '%s%s' % (MAC_CHAIN_PREFIX, vif)
def _delete_vif_mac_rules(vif, current_rules):
chain = _mac_chain_name(vif)
for rule in current_rules:
if '-i %s' % vif in rule and '--among-src' in rule:
ebtables(['-D', chain] + rule.split())
def _delete_mac_spoofing_protection(vifs, current_rules):
# delete the jump rule and then delete the whole chain
jumps = [vif for vif in vifs
if _mac_vif_jump_present(vif, current_rules)]
for vif in jumps:
ebtables(['-D', 'FORWARD', '-i', vif, '-j',
_mac_chain_name(vif)])
for vif in vifs:
chain = _mac_chain_name(vif)
if chain_exists(chain, current_rules):
ebtables(['-X', chain])
# Used to scope ebtables commands in testing
NAMESPACE = None

View File

@ -14,6 +14,7 @@
# under the License.
from neutron.common import constants
from neutron.common import utils
from neutron.plugins.ml2.drivers.linuxbridge.agent import arp_protect
from neutron.tests.common import machine_fixtures
from neutron.tests.common import net_helpers
@ -34,10 +35,17 @@ class LinuxBridgeARPSpoofTestCase(functional_base.BaseSudoTestCase):
bridge = lbfixture.bridge
self.source, self.destination, self.observer = self.useFixture(
machine_fixtures.PeerMachines(bridge, amount=3)).machines
self.addCleanup(self._ensure_rules_cleaned)
def _ensure_rules_cleaned(self):
rules = [r for r in arp_protect.ebtables(['-L']).splitlines()
if r and 'Bridge' not in r]
self.assertEqual([], rules, 'Test leaked ebtables rules')
def _add_arp_protection(self, machine, addresses, extra_port_dict=None):
port_dict = {'fixed_ips': [{'ip_address': a} for a in addresses],
'device_owner': 'nobody'}
'device_owner': 'nobody',
'mac_address': machine.port.link.address}
if extra_port_dict:
port_dict.update(extra_port_dict)
name = net_helpers.VethFixture.get_peer_name(machine.port.name)
@ -55,12 +63,38 @@ class LinuxBridgeARPSpoofTestCase(functional_base.BaseSudoTestCase):
arping(self.source.namespace, self.destination.ip)
arping(self.destination.namespace, self.source.ip)
def test_arp_correct_protection_allowed_address_pairs(self):
smac = self.source.port.link.address
port = {'mac_address': '00:11:22:33:44:55',
'allowed_address_pairs': [{'mac_address': smac,
'ip_address': self.source.ip}]}
# make sure a large number of allowed address pairs works
for i in range(100000):
port['allowed_address_pairs'].append(
{'mac_address': utils.get_random_mac(
'fa:16:3e:00:00:00'.split(':')),
'ip_address': '10.10.10.10'})
self._add_arp_protection(self.source, ['1.2.2.2'], port)
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_fails_incorrect_mac_protection(self):
# a bad mac filter on the source will prevent any traffic from it
self._add_arp_protection(self.source, [self.source.ip],
{'mac_address': '00:11:22:33:44:55'})
no_arping(self.source.namespace, self.destination.ip)
no_arping(self.destination.namespace, self.source.ip)
# correcting it should make it work
self._add_arp_protection(self.source, [self.source.ip])
arping(self.source.namespace, self.destination.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'])