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:
parent
ab614a10a7
commit
be298f8bc3
|
@ -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
|
||||
|
||||
|
|
|
@ -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'])
|
||||
|
|
Loading…
Reference in New Issue