Prevent binding IPv6 addresses to Neutron interfaces
Explicitly disable IPv6 on Neutron created interfaces in the default
namespace before setting link up. Since the default behavior of IPv6 is
to bind to all interfaces as opposed to IPv4 where an address must be
explicitly configured we disable IPv6 on each interface before enabling
the interface. This avoids leaving a time window between when the
interface is enabled and when it is attached to bridge device during
which the host could be access from a tenant network.
Move disable_ipv6() from BridgeDevice to base IPDevice class so it is
usable by all interfaces. Then we explicitly disable IPv6 on veth
interfaces in the default namespaces and VXLAN and VLAN interfaces
created by the LinuxBridge agent.
In addition vlan interface is moved from LinuxBridgeManager to IPWrapper
so it can return an IPDevice object.
Conflicts:
neutron/agent/linux/bridge_lib.py
neutron/tests/unit/plugins/ml2/drivers/linuxbridge/agent/test_linuxbridge_neutron_agent.py
Closes-Bug: #1534652
Change-Id: Id879075f2d5ee42f8ff153e813e7519a4424447b
(cherry picked from commit fc8ebae035
)
This commit is contained in:
parent
7442bfd4ea
commit
ccdfd17666
|
@ -16,12 +16,7 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from oslo_log import log as logging
|
||||
|
||||
from neutron.agent.linux import ip_lib
|
||||
from neutron.i18n import _LE
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BridgeDevice(ip_lib.IPDevice):
|
||||
|
@ -30,32 +25,6 @@ class BridgeDevice(ip_lib.IPDevice):
|
|||
ip_wrapper = ip_lib.IPWrapper(self.namespace)
|
||||
return ip_wrapper.netns.execute(cmd, run_as_root=True)
|
||||
|
||||
def _sysctl(self, cmd):
|
||||
"""execute() doesn't return the exit status of the command it runs,
|
||||
it returns stdout and stderr. Setting check_exit_code=True will cause
|
||||
it to raise a RuntimeError if the exit status of the command is
|
||||
non-zero, which in sysctl's case is an error. So we're normalizing
|
||||
that into zero (success) and one (failure) here to mimic what
|
||||
"echo $?" in a shell would be.
|
||||
|
||||
This is all because sysctl is too verbose and prints the value you
|
||||
just set on success, unlike most other utilities that print nothing.
|
||||
|
||||
execute() will have dumped a message to the logs with the actual
|
||||
output on failure, so it's not lost, and we don't need to print it
|
||||
here.
|
||||
"""
|
||||
cmd = ['sysctl', '-w'] + cmd
|
||||
ip_wrapper = ip_lib.IPWrapper(self.namespace)
|
||||
try:
|
||||
ip_wrapper.netns.execute(cmd, run_as_root=True,
|
||||
check_exit_code=True)
|
||||
except RuntimeError:
|
||||
LOG.exception(_LE("Failed running %s"), cmd)
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
@classmethod
|
||||
def addbr(cls, name, namespace=None):
|
||||
bridge = cls(name, namespace)
|
||||
|
@ -76,7 +45,3 @@ class BridgeDevice(ip_lib.IPDevice):
|
|||
|
||||
def disable_stp(self):
|
||||
return self._brctl(['stp', self.name, 'off'])
|
||||
|
||||
def disable_ipv6(self):
|
||||
cmd = 'net.ipv6.conf.%s.disable_ipv6=1' % self.name
|
||||
return self._sysctl([cmd])
|
||||
|
|
|
@ -328,6 +328,7 @@ class OVSInterfaceDriver(LinuxInterfaceDriver):
|
|||
root_dev, ns_dev = ip.add_veth(tap_name,
|
||||
device_name,
|
||||
namespace2=namespace)
|
||||
root_dev.disable_ipv6()
|
||||
else:
|
||||
ns_dev = ip.device(device_name)
|
||||
|
||||
|
@ -435,6 +436,7 @@ class IVSInterfaceDriver(LinuxInterfaceDriver):
|
|||
tap_name = self._get_tap_name(device_name, prefix)
|
||||
|
||||
root_dev, ns_dev = ip.add_veth(tap_name, device_name)
|
||||
root_dev.disable_ipv6()
|
||||
|
||||
self._ivs_add_port(tap_name, port_id, mac_address)
|
||||
|
||||
|
@ -482,6 +484,7 @@ class BridgeInterfaceDriver(LinuxInterfaceDriver):
|
|||
# Create ns_veth in a namespace if one is configured.
|
||||
root_veth, ns_veth = ip.add_veth(tap_name, device_name,
|
||||
namespace2=namespace)
|
||||
root_veth.disable_ipv6()
|
||||
ns_veth.link.set_address(mac_address)
|
||||
|
||||
if self.conf.network_device_mtu:
|
||||
|
|
|
@ -203,6 +203,12 @@ class IPWrapper(SubProcessBase):
|
|||
if self.namespace:
|
||||
device.link.set_netns(self.namespace)
|
||||
|
||||
def add_vlan(self, name, physical_interface, vlan_id):
|
||||
cmd = ['add', 'link', physical_interface, 'name', name,
|
||||
'type', 'vlan', 'id', vlan_id]
|
||||
self._as_root([], 'link', cmd)
|
||||
return IPDevice(name, namespace=self.namespace)
|
||||
|
||||
def add_vxlan(self, name, vni, group=None, dev=None, ttl=None, tos=None,
|
||||
local=None, port=None, proxy=False):
|
||||
cmd = ['add', name, 'type', 'vxlan', 'id', vni]
|
||||
|
@ -284,6 +290,37 @@ class IPDevice(SubProcessBase):
|
|||
LOG.exception(_LE("Failed deleting egress connection state of"
|
||||
" floatingip %s"), ip_str)
|
||||
|
||||
def _sysctl(self, cmd):
|
||||
"""execute() doesn't return the exit status of the command it runs,
|
||||
it returns stdout and stderr. Setting check_exit_code=True will cause
|
||||
it to raise a RuntimeError if the exit status of the command is
|
||||
non-zero, which in sysctl's case is an error. So we're normalizing
|
||||
that into zero (success) and one (failure) here to mimic what
|
||||
"echo $?" in a shell would be.
|
||||
|
||||
This is all because sysctl is too verbose and prints the value you
|
||||
just set on success, unlike most other utilities that print nothing.
|
||||
|
||||
execute() will have dumped a message to the logs with the actual
|
||||
output on failure, so it's not lost, and we don't need to print it
|
||||
here.
|
||||
"""
|
||||
cmd = ['sysctl', '-w'] + cmd
|
||||
ip_wrapper = IPWrapper(self.namespace)
|
||||
try:
|
||||
ip_wrapper.netns.execute(cmd, run_as_root=True,
|
||||
check_exit_code=True)
|
||||
except RuntimeError:
|
||||
LOG.exception(_LE("Failed running %s"), cmd)
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
def disable_ipv6(self):
|
||||
sysctl_name = re.sub(r'\.', '/', self.name)
|
||||
cmd = 'net.ipv6.conf.%s.disable_ipv6=1' % sysctl_name
|
||||
return self._sysctl([cmd])
|
||||
|
||||
|
||||
class IpCommandBase(object):
|
||||
COMMAND = ''
|
||||
|
@ -870,6 +907,14 @@ class IpNetnsCommand(IpCommandBase):
|
|||
return False
|
||||
|
||||
|
||||
def vlan_in_use(segmentation_id, namespace=None):
|
||||
"""Return True if VLAN ID is in use by an interface, else False."""
|
||||
ip_wrapper = IPWrapper(namespace=namespace)
|
||||
interfaces = ip_wrapper.netns.execute(["ip", "-d", "link", "list"],
|
||||
check_exit_code=True)
|
||||
return '802.1Q id %s ' % segmentation_id in interfaces
|
||||
|
||||
|
||||
def vxlan_in_use(segmentation_id, namespace=None):
|
||||
"""Return True if VXLAN VNID is in use by an interface, else False."""
|
||||
ip_wrapper = IPWrapper(namespace=namespace)
|
||||
|
|
|
@ -293,14 +293,19 @@ class LinuxBridgeManager(object):
|
|||
"%(physical_interface)s",
|
||||
{'interface': interface, 'vlan_id': vlan_id,
|
||||
'physical_interface': physical_interface})
|
||||
if utils.execute(['ip', 'link', 'add', 'link',
|
||||
physical_interface,
|
||||
'name', interface, 'type', 'vlan', 'id',
|
||||
vlan_id], run_as_root=True):
|
||||
return
|
||||
if utils.execute(['ip', 'link', 'set',
|
||||
interface, 'up'], run_as_root=True):
|
||||
return
|
||||
try:
|
||||
int_vlan = self.ip.add_vlan(interface, physical_interface,
|
||||
vlan_id)
|
||||
except RuntimeError:
|
||||
with excutils.save_and_reraise_exception() as ctxt:
|
||||
if ip_lib.vlan_in_use(vlan_id):
|
||||
ctxt.reraise = False
|
||||
LOG.error(_LE("Unable to create VLAN interface for "
|
||||
"VLAN ID %s because it is in use by "
|
||||
"another interface."), vlan_id)
|
||||
return
|
||||
int_vlan.disable_ipv6()
|
||||
int_vlan.link.set_up()
|
||||
LOG.debug("Done creating subinterface %s", interface)
|
||||
return interface
|
||||
|
||||
|
@ -334,6 +339,7 @@ class LinuxBridgeManager(object):
|
|||
"VNI %s because it is in use by another "
|
||||
"interface."), segmentation_id)
|
||||
return None
|
||||
int_vxlan.disable_ipv6()
|
||||
int_vxlan.link.set_up()
|
||||
LOG.debug("Done creating vxlan interface %s", interface)
|
||||
return interface
|
||||
|
|
|
@ -435,6 +435,17 @@ class TestIpWrapper(base.BaseTestCase):
|
|||
self.assertNotIn(mock.call().delete('ns'),
|
||||
ip_ns_cmd_cls.mock_calls)
|
||||
|
||||
def test_add_vlan(self):
|
||||
retval = ip_lib.IPWrapper().add_vlan('eth0.1', 'eth0', '1')
|
||||
self.assertIsInstance(retval, ip_lib.IPDevice)
|
||||
self.assertEqual(retval.name, 'eth0.1')
|
||||
self.execute.assert_called_once_with([], 'link',
|
||||
['add', 'link', 'eth0',
|
||||
'name', 'eth0.1',
|
||||
'type', 'vlan', 'id', '1'],
|
||||
run_as_root=True, namespace=None,
|
||||
log_fail_as_error=True)
|
||||
|
||||
def test_add_vxlan_valid_port_length(self):
|
||||
retval = ip_lib.IPWrapper().add_vxlan('vxlan0', 'vni0',
|
||||
group='group0',
|
||||
|
|
|
@ -52,6 +52,9 @@ class FakeIpDevice(object):
|
|||
def __init__(self):
|
||||
self.link = FakeIpLinkCommand()
|
||||
|
||||
def disable_ipv6(self):
|
||||
pass
|
||||
|
||||
|
||||
def get_linuxbridge_manager(bridge_mappings, interface_mappings):
|
||||
with mock.patch.object(ip_lib.IPWrapper, 'get_device_by_ip',
|
||||
|
@ -729,14 +732,14 @@ class TestLinuxBridgeManager(base.BaseTestCase):
|
|||
de_fn.return_value = True
|
||||
self.assertEqual(self.lbm.ensure_vlan("eth0", "1"), "eth0.1")
|
||||
de_fn.return_value = False
|
||||
with mock.patch.object(utils, 'execute') as exec_fn:
|
||||
exec_fn.return_value = False
|
||||
self.assertEqual(self.lbm.ensure_vlan("eth0", "1"), "eth0.1")
|
||||
# FIXME(kevinbenton): validate the params to the exec_fn calls
|
||||
self.assertEqual(exec_fn.call_count, 2)
|
||||
exec_fn.return_value = True
|
||||
self.assertIsNone(self.lbm.ensure_vlan("eth0", "1"))
|
||||
self.assertEqual(exec_fn.call_count, 3)
|
||||
vlan_dev = FakeIpDevice()
|
||||
with mock.patch.object(vlan_dev, 'disable_ipv6') as dv6_fn,\
|
||||
mock.patch.object(self.lbm.ip, 'add_vlan',
|
||||
return_value=vlan_dev) as add_vlan_fn:
|
||||
retval = self.lbm.ensure_vlan("eth0", "1")
|
||||
self.assertEqual("eth0.1", retval)
|
||||
add_vlan_fn.assert_called_with('eth0.1', 'eth0', '1')
|
||||
dv6_fn.assert_called_once_with()
|
||||
|
||||
def test_ensure_vxlan(self):
|
||||
seg_id = "12345678"
|
||||
|
@ -746,14 +749,16 @@ class TestLinuxBridgeManager(base.BaseTestCase):
|
|||
de_fn.return_value = True
|
||||
self.assertEqual(self.lbm.ensure_vxlan(seg_id), "vxlan-" + seg_id)
|
||||
de_fn.return_value = False
|
||||
with mock.patch.object(self.lbm.ip,
|
||||
'add_vxlan') as add_vxlan_fn:
|
||||
add_vxlan_fn.return_value = FakeIpDevice()
|
||||
self.assertEqual(self.lbm.ensure_vxlan(seg_id),
|
||||
"vxlan-" + seg_id)
|
||||
vxlan_dev = FakeIpDevice()
|
||||
with mock.patch.object(vxlan_dev, 'disable_ipv6') as dv6_fn,\
|
||||
mock.patch.object(self.lbm.ip, 'add_vxlan',
|
||||
return_value=vxlan_dev) as add_vxlan_fn:
|
||||
retval = self.lbm.ensure_vxlan(seg_id)
|
||||
self.assertEqual("vxlan-" + seg_id, retval)
|
||||
add_vxlan_fn.assert_called_with("vxlan-" + seg_id, seg_id,
|
||||
group="224.0.0.1",
|
||||
dev=self.lbm.local_int)
|
||||
dv6_fn.assert_called_once_with()
|
||||
cfg.CONF.set_override('l2_population', 'True', 'VXLAN')
|
||||
self.assertEqual(self.lbm.ensure_vxlan(seg_id),
|
||||
"vxlan-" + seg_id)
|
||||
|
|
Loading…
Reference in New Issue