diff options
Diffstat (limited to 'test/test_ip_ecmp.py')
-rw-r--r-- | test/test_ip_ecmp.py | 252 |
1 files changed, 252 insertions, 0 deletions
diff --git a/test/test_ip_ecmp.py b/test/test_ip_ecmp.py new file mode 100644 index 00000000000..60673ea6f3a --- /dev/null +++ b/test/test_ip_ecmp.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python + +import unittest +import random +import socket +from ipaddress import IPv4Address, IPv6Address, AddressValueError + +from framework import VppTestCase, VppTestRunner +from util import ppp + +from scapy.packet import Raw +from scapy.layers.l2 import Ether +from scapy.layers.inet import IP, UDP +from scapy.layers.inet6 import IPv6 + +# +# The number of packets to sent. +# +N_PKTS_IN_STREAM = 300 + + +class TestECMP(VppTestCase): + """ Equal-cost multi-path routing Test Case """ + + @classmethod + def setUpClass(cls): + """ + Perform standard class setup (defined by class method setUpClass in + class VppTestCase) before running the test case, set test case related + variables and configure VPP. + """ + super(TestECMP, cls).setUpClass() + + # create 4 pg interfaces + cls.create_pg_interfaces(range(4)) + + # packet sizes to test + cls.pg_if_packet_sizes = [64, 1500, 9018] + + # setup interfaces + for i in cls.pg_interfaces: + i.admin_up() + i.generate_remote_hosts(5) + i.config_ip4() + i.resolve_arp() + i.configure_ipv4_neighbors() + i.config_ip6() + i.resolve_ndp() + i.configure_ipv6_neighbors() + + @classmethod + def tearDownClass(cls): + if not cls.vpp_dead: + for i in cls.pg_interfaces: + i.unconfig_ip4() + i.unconfig_ip6() + i.admin_down() + + super(TestECMP, cls).tearDownClass() + + def setUp(self): + super(TestECMP, self).setUp() + self.reset_packet_infos() + + def tearDown(self): + """ + Show various debug prints after each test. + """ + super(TestECMP, self).tearDown() + if not self.vpp_dead: + self.logger.info(self.vapi.ppcli("show ip arp")) + self.logger.info(self.vapi.ppcli("show ip6 neighbors")) + + def get_ip_address(self, ip_addr_start, ip_prefix_len): + """ + + :param str ip_addr_start: Starting IPv4 or IPv6 address. + :param int ip_prefix_len: IP address prefix length. + :return: Random IPv4 or IPv6 address from required range. + """ + try: + ip_addr = IPv4Address(unicode(ip_addr_start)) + ip_max_len = 32 + except (AttributeError, AddressValueError): + ip_addr = IPv6Address(unicode(ip_addr_start)) + ip_max_len = 128 + + return str(ip_addr + + random.randint(0, 2 ** (ip_max_len - ip_prefix_len) - 2)) + + def create_stream(self, src_if, src_ip_start, dst_ip_start, + ip_prefix_len, packet_sizes, ip_l=IP): + """Create input packet stream for defined interfaces. + + :param VppInterface src_if: Source Interface for packet stream. + :param str src_ip_start: Starting source IPv4 or IPv6 address. + :param str dst_ip_start: Starting destination IPv4 or IPv6 address. + :param int ip_prefix_len: IP address prefix length. + :param list packet_sizes: packet size to test. + :param Scapy ip_l: Required IP layer - IP or IPv6. (Default is IP.) + """ + pkts = [] + for i in range(0, N_PKTS_IN_STREAM): + info = self.create_packet_info(src_if, src_if) + payload = self.info_to_payload(info) + src_ip = self.get_ip_address(src_ip_start, ip_prefix_len) + dst_ip = self.get_ip_address(dst_ip_start, ip_prefix_len) + p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / + ip_l(src=src_ip, dst=dst_ip) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + info.data = p.copy() + size = random.choice(packet_sizes) + self.extend_packet(p, size) + pkts.append(p) + return pkts + + def verify_capture(self, rx_if, capture, ip_l=IP): + """Verify captured input packet stream for defined interface. + + :param VppInterface rx_if: Interface to verify captured packet stream. + :param list capture: Captured packet stream. + :param Scapy ip_l: Required IP layer - IP or IPv6. (Default is IP.) + """ + self.logger.info("Verifying capture on interface %s" % rx_if.name) + + count = 0 + host_counters = {} + for host_mac in rx_if._hosts_by_mac: + host_counters[host_mac] = 0 + + for packet in capture: + try: + ip_received = packet[ip_l] + payload_info = self.payload_to_info(str(packet[Raw])) + packet_index = payload_info.index + ip_sent = self._packet_infos[packet_index].data[ip_l] + self.logger.debug("Got packet on port %s: src=%u (id=%u)" % + (rx_if.name, payload_info.src, packet_index)) + # Check standard fields + self.assertIn(packet.dst, rx_if._hosts_by_mac, + "Destination MAC address %s shouldn't be routed " + "via interface %s" % (packet.dst, rx_if.name)) + self.assertEqual(packet.src, rx_if.local_mac) + self.assertEqual(ip_received.src, ip_sent.src) + self.assertEqual(ip_received.dst, ip_sent.dst) + host_counters[packet.dst] += 1 + self._packet_infos.pop(packet_index) + + except: + self.logger.error(ppp("Unexpected or invalid packet:", packet)) + raise + + # We expect packet routed via all host of pg interface + for host_mac in host_counters: + nr = host_counters[host_mac] + self.assertNotEqual( + nr, 0, "No packet routed via host %s" % host_mac) + self.logger.info("%u packets routed via host %s of %s interface" % + (nr, host_mac, rx_if.name)) + count += nr + self.logger.info("Total amount of %u packets routed via %s interface" % + (count, rx_if.name)) + + return count + + def create_ip_routes(self, dst_ip_net, dst_prefix_len, is_ipv6=0): + """ + Create IP routes for defined destination IP network. + + :param str dst_ip_net: Destination IP network. + :param int dst_prefix_len: IP address prefix length. + :param int is_ipv6: 0 if an ip4 route, else ip6 + """ + af = socket.AF_INET if is_ipv6 == 0 else socket.AF_INET6 + dst_ip = socket.inet_pton(af, dst_ip_net) + + for pg_if in self.pg_interfaces[1:]: + for nh_host in pg_if.remote_hosts: + nh_host_ip = nh_host.ip4 if is_ipv6 == 0 else nh_host.ip6 + next_hop_address = socket.inet_pton(af, nh_host_ip) + next_hop_sw_if_index = pg_if.sw_if_index + self.vapi.ip_add_del_route( + dst_ip, dst_prefix_len, next_hop_address, + next_hop_sw_if_index=next_hop_sw_if_index, + is_ipv6=is_ipv6, is_multipath=1) + self.logger.info("Route via %s on %s created" % + (nh_host_ip, pg_if.name)) + + self.logger.debug(self.vapi.ppcli("show ip fib")) + self.logger.debug(self.vapi.ppcli("show ip6 fib")) + + def test_ip_ecmp(self): + """ IP equal-cost multi-path routing test """ + + src_ip_net = '16.0.0.1' + dst_ip_net = '32.0.0.1' + ip_prefix_len = 24 + + self.create_ip_routes(dst_ip_net, ip_prefix_len) + + pkts = self.create_stream(self.pg0, src_ip_net, dst_ip_net, + ip_prefix_len, self.pg_if_packet_sizes) + self.pg0.add_stream(pkts) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # We expect packets on pg1, pg2 and pg3, but not on pg0 + rx_count = 0 + for pg_if in self.pg_interfaces[1:]: + capture = pg_if._get_capture(timeout=1) + self.assertNotEqual( + len(capture), 0, msg="No packets captured on %s" % pg_if.name) + rx_count += self.verify_capture(pg_if, capture) + self.pg0.assert_nothing_captured(remark="IP packets forwarded on pg0") + + # Check that all packets were forwarded via pg1, pg2 and pg3 + self.assertEqual(rx_count, len(pkts)) + + def test_ip6_ecmp(self): + """ IPv6 equal-cost multi-path routing test """ + + src_ip_net = '3ffe:51::1' + dst_ip_net = '3ffe:71::1' + ip_prefix_len = 64 + + self.create_ip_routes(dst_ip_net, ip_prefix_len, is_ipv6=1) + + pkts = self.create_stream( + self.pg0, src_ip_net, dst_ip_net, + ip_prefix_len, self.pg_if_packet_sizes, ip_l=IPv6) + self.pg0.add_stream(pkts) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # We expect packets on pg1, pg2 and pg3, but not on pg0 + rx_count = 0 + for pg_if in self.pg_interfaces[1:]: + capture = pg_if._get_capture(timeout=1) + self.assertNotEqual( + len(capture), 0, msg="No packets captured on %s" % pg_if.name) + rx_count += self.verify_capture(pg_if, capture, ip_l=IPv6) + self.pg0.assert_nothing_captured(remark="IP packets forwarded on pg0") + + # Check that all packets were forwarded via pg1, pg2 and pg3 + self.assertEqual(rx_count, len(pkts)) + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) |