2019-10-31 13:31:07 -05:00
|
|
|
#!/usr/bin/env python3
|
2018-07-13 16:38:04 +02:00
|
|
|
|
|
|
|
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
|
2018-05-01 05:17:55 -07:00
|
|
|
from vpp_ip_route import VppIpRoute, VppRoutePath
|
2018-07-13 16:38:04 +02:00
|
|
|
|
2018-11-25 16:09:26 -08:00
|
|
|
try:
|
|
|
|
text_type = unicode
|
|
|
|
except NameError:
|
|
|
|
text_type = str
|
|
|
|
|
2018-07-13 16:38:04 +02:00
|
|
|
#
|
|
|
|
# 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()
|
2019-03-13 09:23:05 -07:00
|
|
|
|
|
|
|
def show_commands_at_teardown(self):
|
2019-09-30 10:53:31 +00:00
|
|
|
self.logger.info(self.vapi.ppcli("show ip4 neighbors"))
|
2019-03-13 09:23:05 -07:00
|
|
|
self.logger.info(self.vapi.ppcli("show ip6 neighbors"))
|
2018-07-13 16:38:04 +02:00
|
|
|
|
|
|
|
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:
|
2018-11-25 16:09:26 -08:00
|
|
|
ip_addr = IPv4Address(text_type(ip_addr_start))
|
2018-07-13 16:38:04 +02:00
|
|
|
ip_max_len = 32
|
|
|
|
except (AttributeError, AddressValueError):
|
2018-11-25 16:09:26 -08:00
|
|
|
ip_addr = IPv6Address(text_type(ip_addr_start))
|
2018-07-13 16:38:04 +02:00
|
|
|
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]
|
2019-03-06 11:58:06 -08:00
|
|
|
payload_info = self.payload_to_info(packet[Raw])
|
2018-07-13 16:38:04 +02:00
|
|
|
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
|
|
|
|
"""
|
|
|
|
|
2018-05-01 05:17:55 -07:00
|
|
|
paths = []
|
2018-07-13 16:38:04 +02:00
|
|
|
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
|
2018-05-01 05:17:55 -07:00
|
|
|
paths.append(VppRoutePath(nh_host_ip,
|
|
|
|
pg_if.sw_if_index))
|
|
|
|
|
|
|
|
rip = VppIpRoute(self, dst_ip_net, dst_prefix_len, paths)
|
|
|
|
rip.add_vpp_config()
|
|
|
|
self.logger.info("Route via %s on %s created" %
|
|
|
|
(nh_host_ip, pg_if.name))
|
2018-07-13 16:38:04 +02:00
|
|
|
|
|
|
|
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)
|