From 5f99c79a504264397f3784abf295ba624241b380 Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Fri, 26 Apr 2019 16:56:23 +0000 Subject: [PATCH] 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 --- neutron/agent/linux/tc_lib.py | 29 +++++ .../functional/agent/linux/test_tc_lib.py | 104 ++++++++++++++++++ neutron/tests/unit/agent/linux/test_tc_lib.py | 9 ++ 3 files changed, 142 insertions(+) diff --git a/neutron/agent/linux/tc_lib.py b/neutron/agent/linux/tc_lib.py index 952bfbaf89d..3131cfb4bf6 100644 --- a/neutron/agent/linux/tc_lib.py +++ b/neutron/agent/linux/tc_lib.py @@ -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. diff --git a/neutron/tests/functional/agent/linux/test_tc_lib.py b/neutron/tests/functional/agent/linux/test_tc_lib.py index cc14329982c..11265066b2a 100644 --- a/neutron/tests/functional/agent/linux/test_tc_lib.py +++ b/neutron/tests/functional/agent/linux/test_tc_lib.py @@ -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']) diff --git a/neutron/tests/unit/agent/linux/test_tc_lib.py b/neutron/tests/unit/agent/linux/test_tc_lib.py index aafd6c57ab7..128503a627f 100644 --- a/neutron/tests/unit/agent/linux/test_tc_lib.py +++ b/neutron/tests/unit/agent/linux/test_tc_lib.py @@ -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')