Add TC filtering for VXLAN traffic

This new function in neutron.agent.linux.tc_lib creates a TC filter in
a device depending on the VXLAN ID (VNI) and the source MAC address
(usually the VM TAP MAC address). This filter will send all the egress
tunneled traffic from the VM to a TC class in other to shape it (QoS).

Change-Id: Ic04b52bc0aca7a18fa06ea89e981c80d67f42eb1
Related-Bug: #1560963
This commit is contained in:
Rodolfo Alonso Hernandez 2019-04-26 16:56:23 +00:00
parent dfc2586fb1
commit 5f99c79a50
3 changed files with 142 additions and 0 deletions

View File

@ -58,6 +58,17 @@ TC_QDISC_PARENT = {'root': rtnl.TC_H_ROOT,
'ingress': rtnl.TC_H_INGRESS}
TC_QDISC_PARENT_NAME = {v: k for k, v in TC_QDISC_PARENT.items()}
TC_CLASS_MAX_FLOWID = 0xffff
# NOTE(ralonsoh): VXLAN header: +28 bytes from the outer MAC header (TC
# initial offset)
# - VXLAN flags: 1 byte
# - Reserved: 3 bytes
# - VNI: 3 bytes --> VXLAN_VNI_OFFSET = 32 (+32 from the TC initial offset)
# - Reserved: 1 byte
VXLAN_INNER_SRC_MAC_OFFSET = 42
VXLAN_VNI_OFFSET = 32
class InvalidKernelHzValue(exceptions.NeutronException):
message = _("Kernel HZ value %(value)s is not valid. This value must be "
@ -469,6 +480,24 @@ def delete_tc_policy_class(device, parent, classid, namespace=None):
namespace=namespace)
def add_tc_filter_vxlan(device, parent, classid, src_mac, vxlan_id,
namespace=None):
"""Add a TC filter to match VXLAN traffic based on the VM mac and the VNI.
:param device: (string) device name
:param parent: (string) qdisc parent class ('root', 'ingress', '2:10')
:param classid: (string) major:minor handler identifier ('10:20')
:param src_mac: (string) source MAC address to match (VM mac)
:param vxlan_id: (int) VXLAN ID (VNI)
:param namespace: (string) (optional) namespace name
"""
keys = [hex(int(vxlan_id << 8)) + '/0xffffff00+' + str(VXLAN_VNI_OFFSET)]
keys += [key['key'] for key in
_mac_to_pyroute2_keys(src_mac, VXLAN_INNER_SRC_MAC_OFFSET)]
priv_tc_lib.add_tc_filter_match32(device, parent, 1, classid, keys,
namespace=namespace)
def add_tc_filter_match_mac(device, parent, classid, mac, offset=0, priority=0,
protocol=None, namespace=None):
"""Add a TC filter in a device to match a MAC address.

View File

@ -13,9 +13,12 @@
# License for the specific language governing permissions and limitations
# under the License.
import random
import netaddr
from oslo_utils import uuidutils
from neutron.agent.linux import bridge_lib
from neutron.agent.linux import ip_lib
from neutron.agent.linux import tc_lib
from neutron.privileged.agent.linux import ip_lib as priv_ip_lib
@ -135,3 +138,104 @@ class TcPolicyClassTestCase(functional_base.BaseSudoTestCase):
namespace=self.ns[0])
self.assertGreater(tc_classes[0]['stats']['bytes'], bytes)
self.assertGreater(tc_classes[0]['stats']['packets'], packets)
class TcFiltersTestCase(functional_base.BaseSudoTestCase):
def _remove_ns(self, namespace):
priv_ip_lib.remove_netns(namespace)
def _create_two_namespaces_connected_using_vxlan(self):
"""Create two namespaces connected with a veth pair and VXLAN
--------------------------------- ----------------------------------
(ns1) | | (ns2)
int1: 10.0.100.1/24 <-----------|----|------------> int2: 10.0.100.2/24
| | | |
|> int1_vxlan1: 10.0.200.1/24 | | int1_vxlan2: 10.0.200.2/24 <|
--------------------------------- ----------------------------------
"""
self.vxlan_id = 100
self.ns = ['ns1_' + uuidutils.generate_uuid(),
'ns2_' + uuidutils.generate_uuid()]
self.device = ['int1', 'int2']
self.device_vxlan = ['int_vxlan1', 'int_vxlan2']
self.mac_vxlan = []
self.ip = ['10.100.0.1/24', '10.100.0.2/24']
self.ip_vxlan = ['10.200.0.1/24', '10.200.0.2/24']
for i in range(len(self.ns)):
priv_ip_lib.create_netns(self.ns[i])
self.addCleanup(self._remove_ns, self.ns[i])
ip_wrapper = ip_lib.IPWrapper(self.ns[i])
if i == 0:
ip_wrapper.add_veth(self.device[0], self.device[1], self.ns[1])
ip_wrapper.add_vxlan(self.device_vxlan[i], self.vxlan_id,
dev=self.device[i])
ip_device = ip_lib.IPDevice(self.device[i], self.ns[i])
ip_device.link.set_up()
ip_device.addr.add(self.ip[i])
ip_device_vxlan = ip_lib.IPDevice(self.device_vxlan[i], self.ns[i])
self.mac_vxlan.append(ip_device_vxlan.link.address)
ip_device_vxlan.link.set_up()
ip_device_vxlan.addr.add(self.ip_vxlan[i])
bridge_lib.FdbInterface.append(
'00:00:00:00:00:00', self.device_vxlan[0], namespace=self.ns[0],
ip_dst=str(netaddr.IPNetwork(self.ip[1]).ip))
bridge_lib.FdbInterface.append(
'00:00:00:00:00:00', self.device_vxlan[1], namespace=self.ns[1],
ip_dst=str(netaddr.IPNetwork(self.ip[0]).ip))
def test_add_tc_filter_vxlan(self):
# The traffic control is applied on the veth pair device of the first
# namespace (self.ns[0]). The traffic created from the VXLAN interface
# when replying to the ping (sent from the other namespace), is
# encapsulated in a VXLAN frame and goes through the veth pair
# interface.
self._create_two_namespaces_connected_using_vxlan()
tc_lib.add_tc_qdisc(self.device[0], 'htb', parent='root', handle='1:',
namespace=self.ns[0])
classes = tc_lib.list_tc_policy_class(self.device[0],
namespace=self.ns[0])
self.assertEqual(0, len(classes))
class_ids = []
for i in range(1, 10):
class_id = '1:%s' % i
class_ids.append(class_id)
tc_lib.add_tc_policy_class(
self.device[0], '1:', class_id, namespace=self.ns[0],
min_kbps=1000, max_kbps=2000, burst_kb=1600)
# Add a filter for a randomly chosen created class, in the first
# namespace veth pair device, with the VXLAN MAC address. The traffic
# from the VXLAN device must go through this chosen class.
chosen_class_id = random.choice(class_ids)
tc_lib.add_tc_filter_vxlan(
self.device[0], '1:', chosen_class_id, self.mac_vxlan[0],
self.vxlan_id, namespace=self.ns[0])
tc_classes = tc_lib.list_tc_policy_class(self.device[0],
namespace=self.ns[0])
for tc_class in (c for c in tc_classes if
c['classid'] == chosen_class_id):
bytes = tc_class['stats']['bytes']
packets = tc_class['stats']['packets']
break
else:
self.fail('TC class %(class_id)s is not present in the device '
'%(device)s' % {'class_id': chosen_class_id,
'device': self.device[0]})
net_helpers.assert_ping(
self.ns[1], netaddr.IPNetwork(self.ip_vxlan[0]).ip, count=1)
tc_classes = tc_lib.list_tc_policy_class(self.device[0],
namespace=self.ns[0])
for tc_class in tc_classes:
if tc_class['classid'] == chosen_class_id:
self.assertGreater(tc_class['stats']['bytes'], bytes)
self.assertGreater(tc_class['stats']['packets'], packets)
else:
self.assertEqual(0, tc_class['stats']['bytes'])
self.assertEqual(0, tc_class['stats']['packets'])

View File

@ -388,3 +388,12 @@ class TcFilterTestCase(base.BaseTestCase):
'key': '0x89ab0000/0xffff0000+14'}
self.assertEqual(high, keys[0])
self.assertEqual(low, keys[1])
@mock.patch.object(priv_tc_lib, 'add_tc_filter_match32')
def test_add_tc_filter_vxlan(self, mock_add_filter):
tc_lib.add_tc_filter_vxlan('device', 'parent', 'classid',
'12:34:56:78:90:ab', 52, namespace='ns')
keys = ['0x3400/0xffffff00+32', '0x12345678/0xffffffff+42',
'0x90ab0000/0xffff0000+46']
mock_add_filter.assert_called_once_with(
'device', 'parent', 1, 'classid', keys, namespace='ns')