aboutsummaryrefslogtreecommitdiffstats
path: root/docs/usecases/networksim.rst
blob: 9324c992f00df8df65ca7f12b3866754b3a1c687 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
Generating traffic with VPP
===========================

Vpp includes a fairly capable network simulator plugin, which can
simulate real-world round-trip times and a configurable network packet
loss rate. It’s perfect for evaluating the performance of a TCP stack
under specified delay/bandwidth/loss conditions.

The “nsim” plugin cross-connects two physical interfaces at layer 2,
introducing the specified delay and network loss parameters.
Reconfiguration on the fly is OK, with the proviso that packets held in
the network simulator scheduling wheel will be lost.

Configuration
-------------

Configuration by debug CLI is simple. First, specify the simulator
configuration: unidirectional delay (half of the desired RTT), the link
bandwidth, and the expected average packet size. These parameters allow
the network simulator allocate the right amount of buffering to produce
the requested delay/bandwidth product.

::

       set nsim delay 25.0 ms bandwidth 10 gbit packet-size 128

To simulate network packet drops, add either “packets-per-drop ” or
“drop-fraction [0.0 … 1.0]” parameters:

::

       set nsim delay 25.0 ms bandwidth 10 gbit packet-size 128 packets-per-drop 10000

Remember to configure the layer-2 cross-connect:

::

       nsim enable-disable <interface-1> <interface-2>

Packet Generator Configuration
------------------------------

Here’s a unit-test configuration for the vpp packet generator:

::

     loop cre
     set int ip address loop0 11.22.33.1/24
     set int state loop0 up

     loop cre
     set int ip address loop1 11.22.34.1/24
     set int state loop1 up

     set nsim delay 1.0 ms bandwidth 10 gbit packet-size 128 packets-per-drop 1000
     nsim enable-disable loop0 loop1

     packet-generator new {
         name s0
         limit 10000
         size 128-128
         interface loop0
         node ethernet-input
         data { IP4: 1.2.3 -> 4.5.6
                UDP: 11.22.33.44 -> 11.22.34.44
                UDP: 1234 -> 2345
                incrementing 114
         }
     }

For extra realism, the network simulator drops any specific packet with
the specified probability. In this example, we see that slight variation
from run to run occurs as it should.

::

       DBGvpp# pa en
       DBGvpp# sh err
          Count                    Node                  Reason
             9991                  nsim                  Packets buffered
                9                  nsim                  Network loss simulation drop packets
             9991             ethernet-input             l3 mac mismatch

       DBGvpp# clear err
       DBGvpp# pa en
       DBGvpp# sh err
       sh err
          Count                    Node                  Reason
             9993                  nsim                  Packets buffered
                7                  nsim                  Network loss simulation drop packets
             9993             ethernet-input             l3 mac mismatch
>386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491
#!/usr/bin/env python3

import binascii
import socket
from socket import AF_INET, AF_INET6
import unittest
import sys
from dataclasses import dataclass

from framework import VppTestCase

from scapy.packet import Raw
from scapy.layers.l2 import Ether
from scapy.layers.inet import IP, UDP, TCP
from scapy.layers.inet6 import IPv6
from util import ppp


@dataclass
class VarMask:
    offset: int
    spec: str


@dataclass
class VarMatch:
    offset: int
    value: int
    length: int


class TestClassifier(VppTestCase):
    @staticmethod
    def _resolve_mask_match(mask_match):
        mask_match = binascii.unhexlify(mask_match)
        mask_match_len = ((len(mask_match) - 1) // 16 + 1) * 16
        mask_match = mask_match + b"\0" * (mask_match_len - len(mask_match))
        return mask_match, mask_match_len

    @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(TestClassifier, cls).setUpClass()
        cls.acl_active_table = ""
        cls.af = AF_INET

    def setUp(self):
        """
        Perform test setup before test case.

        **Config:**
            - create 4 pg interfaces
                - untagged pg0/pg1/pg2 interface
                    pg0 -------> pg1 (IP ACL)
                           \
                            ---> pg2 (MAC ACL))
                             \
                              -> pg3 (PBR)
            - setup interfaces:
                - put it into UP state
                - set IPv4/6 addresses
                - resolve neighbor address using ARP

        :ivar list interfaces: pg interfaces.
        :ivar list pg_if_packet_sizes: packet sizes in test.
        :ivar dict acl_tbl_idx: ACL table index.
        :ivar int pbr_vrfid: VRF id for PBR test.
        """
        self.reset_packet_infos()
        super(TestClassifier, self).setUp()
        if self.af is None:  # l2_acl test case
            return

        # create 4 pg interfaces
        self.create_pg_interfaces(range(4))

        # packet sizes to test
        self.pg_if_packet_sizes = [64, 9018]

        self.interfaces = list(self.pg_interfaces)

        # ACL & PBR vars
        self.acl_tbl_idx = {}
        self.pbr_vrfid = 200

        # setup all interfaces
        for intf in self.interfaces:
            intf.admin_up()
            if self.af == AF_INET:
                intf.config_ip4()
                intf.resolve_arp()
            elif self.af == AF_INET6:
                intf.config_ip6()
                intf.resolve_ndp()

    def tearDown(self):
        """Run standard test teardown and acl related log."""
        if self.af is not None and not self.vpp_dead:
            if self.af == AF_INET:
                self.logger.info(self.vapi.ppcli("show inacl type ip4"))
                self.logger.info(self.vapi.ppcli("show outacl type ip4"))
            elif self.af == AF_INET6:
                self.logger.info(self.vapi.ppcli("show inacl type ip6"))
                self.logger.info(self.vapi.ppcli("show outacl type ip6"))

            self.logger.info(self.vapi.cli("show classify table verbose"))
            self.logger.info(self.vapi.cli("show ip fib"))
            self.logger.info(self.vapi.cli("show error"))

            if self.acl_active_table.endswith("out"):
                self.output_acl_set_interface(
                    self.pg0, self.acl_tbl_idx.get(self.acl_active_table), 0
                )
            elif self.acl_active_table != "":
                self.input_acl_set_interface(
                    self.pg0, self.acl_tbl_idx.get(self.acl_active_table), 0
                )
            self.acl_active_table = ""

            for intf in self.interfaces:
                if self.af == AF_INET:
                    intf.unconfig_ip4()
                elif self.af == AF_INET6:
                    intf.unconfig_ip6()
                intf.admin_down()

        super(TestClassifier, self).tearDown()

    @staticmethod
    def build_mac_match(dst_mac="", src_mac="", ether_type=""):
        """Build MAC ACL match data with hexstring format.

        :param str dst_mac: source MAC address <x:x:x:x:x:x>
        :param str src_mac: destination MAC address <x:x:x:x:x:x>
        :param str ether_type: ethernet type <0-ffff>
        """
        if dst_mac:
            dst_mac = dst_mac.replace(":", "")
        if src_mac:
            src_mac = src_mac.replace(":", "")

        return (
            "{!s:0>12}{!s:0>12}{!s:0>4}".format(dst_mac, src_mac, ether_type)
        ).rstrip()

    @staticmethod
    def build_mac_mask(dst_mac="", src_mac="", ether_type=""):
        """Build MAC ACL mask data with hexstring format.

        :param str dst_mac: source MAC address <0-ffffffffffff>
        :param str src_mac: destination MAC address <0-ffffffffffff>
        :param str ether_type: ethernet type <0-ffff>
        """

        return (
            "{!s:0>12}{!s:0>12}{!s:0>4}".format(dst_mac, src_mac, ether_type)
        ).rstrip()

    @staticmethod
    def build_ip_mask(proto="", src_ip="", dst_ip="", src_port="", dst_port=""):
        """Build IP ACL mask data with hexstring format.

        :param str proto: protocol number <0-ff>
        :param str src_ip: source ip address <0-ffffffff>
        :param str dst_ip: destination ip address <0-ffffffff>
        :param str src_port: source port number <0-ffff>
        :param str dst_port: destination port number <0-ffff>
        """

        return (
            "{!s:0>20}{!s:0>12}{!s:0>8}{!s:0>4}{!s:0>4}".format(
                proto, src_ip, dst_ip, src_port, dst_port
            )
        ).rstrip("0")

    @staticmethod
    def build_ip6_mask(nh="", src_ip="", dst_ip="", src_port="", dst_port=""):
        """Build IPv6 ACL mask data with hexstring format.

        :param str nh: next header number <0-ff>
        :param str src_ip: source ip address <0-ffffffff>
        :param str dst_ip: destination ip address <0-ffffffff>
        :param str src_port: source port number <0-ffff>
        :param str dst_port: destination port number <0-ffff>
        """

        return (
            "{!s:0>14}{!s:0>34}{!s:0>32}{!s:0>4}{!s:0>4}".format(
                nh, src_ip, dst_ip, src_port, dst_port
            )
        ).rstrip("0")

    @staticmethod
    def build_payload_mask(masks):
        payload_mask = ""

        for mask in masks:
            # offset is specified in bytes, convert to hex format.
            length = (mask.offset * 2) + len(mask.spec)
            format_spec = "{!s:0>" + str(length) + "}"
            payload_mask += format_spec.format(mask.spec)

        return payload_mask.rstrip("0")

    @staticmethod
    def build_ip_match(proto=0, src_ip="", dst_ip="", src_port=0, dst_port=0):
        """Build IP ACL match data with hexstring format.

        :param int proto: protocol number with valid option "x"
        :param str src_ip: source ip address with format of "x.x.x.x"
        :param str dst_ip: destination ip address with format of "x.x.x.x"
        :param int src_port: source port number "x"
        :param int dst_port: destination port number "x"
        """
        if src_ip:
            src_ip = binascii.hexlify(socket.inet_aton(src_ip)).decode("ascii")
        if dst_ip:
            dst_ip = binascii.hexlify(socket.inet_aton(dst_ip)).decode("ascii")

        return (
            "{!s:0>20}{!s:0>12}{!s:0>8}{!s:0>4}{!s:0>4}".format(
                hex(proto)[2:], src_ip, dst_ip, hex(src_port)[2:], hex(dst_port)[2:]
            )
        ).rstrip("0")

    @staticmethod
    def build_ip6_match(nh=0, src_ip="", dst_ip="", src_port=0, dst_port=0):
        """Build IPv6 ACL match data with hexstring format.

        :param int nh: next header number with valid option "x"
        :param str src_ip: source ip6 address with format of "xxx:xxxx::xxxx"
        :param str dst_ip: destination ip6 address with format of
            "xxx:xxxx::xxxx"
        :param int src_port: source port number "x"
        :param int dst_port: destination port number "x"
        """
        if src_ip:
            if sys.version_info[0] == 2:
                src_ip = binascii.hexlify(socket.inet_pton(socket.AF_INET6, src_ip))
            else:
                src_ip = socket.inet_pton(socket.AF_INET6, src_ip).hex()

        if dst_ip:
            if sys.version_info[0] == 2:
                dst_ip = binascii.hexlify(socket.inet_pton(socket.AF_INET6, dst_ip))
            else:
                dst_ip = socket.inet_pton(socket.AF_INET6, dst_ip).hex()

        return (
            "{!s:0>14}{!s:0>34}{!s:0>32}{!s:0>4}{!s:0>4}".format(
                hex(nh)[2:], src_ip, dst_ip, hex(src_port)[2:], hex(dst_port)[2:]
            )
        ).rstrip("0")

    @staticmethod
    def build_payload_match(matches):
        payload_match = ""

        for match in matches:
            sval = str(hex(match.value)[2:])
            # offset is specified in bytes, convert to hex format.
            length = (match.offset + match.length) * 2

            format_spec = "{!s:0>" + str(length) + "}"
            payload_match += format_spec.format(sval)

        return payload_match.rstrip("0")

    def create_stream(
        self,
        src_if,
        dst_if,
        packet_sizes,
        proto_l=UDP(sport=1234, dport=5678),
        payload_ex=None,
    ):
        """Create input packet stream for defined interfaces.

        :param VppInterface src_if: Source Interface for packet stream.
        :param VppInterface dst_if: Destination Interface for packet stream.
        :param list packet_sizes: packet size to test.
        :param Scapy proto_l: Required IP protocol. Default protocol is UDP.
        """
        pkts = []

        for size in packet_sizes:
            info = self.create_packet_info(src_if, dst_if)
            payload = self.info_to_payload(info)

            # append any additional payload after info
            if payload_ex is not None:
                payload += payload_ex

            if self.af == AF_INET:
                p = (
                    Ether(dst=src_if.local_mac, src=src_if.remote_mac)
                    / IP(src=src_if.remote_ip4, dst=dst_if.remote_ip4)
                    / proto_l
                    / Raw(payload)
                )
            elif self.af == AF_INET6:
                p = (
                    Ether(dst=src_if.local_mac, src=src_if.remote_mac)
                    / IPv6(src=src_if.remote_ip6, dst=dst_if.remote_ip6)
                    / proto_l
                    / Raw(payload)
                )
            info.data = p.copy()
            self.extend_packet(p, size)
            pkts.append(p)
        return pkts

    def verify_capture(self, dst_if, capture, proto_l=UDP):
        """Verify captured input packet stream for defined interface.

        :param VppInterface dst_if: Interface to verify captured packet stream.
        :param list capture: Captured packet stream.
        :param Scapy proto_l: Required IP protocol. Default protocol is UDP.
        """
        ip_proto = IP
        if self.af == AF_INET6:
            ip_proto = IPv6
        self.logger.info("Verifying capture on interface %s" % dst_if.name)
        last_info = dict()
        for i in self.interfaces:
            last_info[i.sw_if_index] = None
        dst_sw_if_index = dst_if.sw_if_index
        for packet in capture:
            try:
                ip_received = packet[ip_proto]
                proto_received = packet[proto_l]
                payload_info = self.payload_to_info(packet[Raw])
                packet_index = payload_info.index
                self.assertEqual(payload_info.dst, dst_sw_if_index)
                self.logger.debug(
                    "Got packet on port %s: src=%u (id=%u)"
                    % (dst_if.name, payload_info.src, packet_index)
                )
                next_info = self.get_next_packet_info_for_interface2(
                    payload_info.src, dst_sw_if_index, last_info[payload_info.src]
                )
                last_info[payload_info.src] = next_info
                self.assertTrue(next_info is not None)
                self.assertEqual(packet_index, next_info.index)
                saved_packet = next_info.data
                ip_saved = saved_packet[ip_proto]
                proto_saved = saved_packet[proto_l]
                # Check standard fields
                self.assertEqual(ip_received.src, ip_saved.src)
                self.assertEqual(ip_received.dst, ip_saved.dst)
                self.assertEqual(proto_received.sport, proto_saved.sport)
                self.assertEqual(proto_received.dport, proto_saved.dport)
            except BaseException:
                self.logger.error(ppp("Unexpected or invalid packet:", packet))
                raise
        for i in self.interfaces:
            remaining_packet = self.get_next_packet_info_for_interface2(
                i.sw_if_index, dst_sw_if_index, last_info[i.sw_if_index]
            )
            self.assertTrue(
                remaining_packet is None,
                "Interface %s: Packet expected from interface %s "
                "didn't arrive" % (dst_if.name, i.name),
            )

    def create_classify_table(self, key, mask, data_offset=0, next_table_index=None):
        """Create Classify Table

        :param str key: key for classify table (ex, ACL name).
        :param str mask: mask value for interested traffic.
        :param int data_offset:
        :param str next_table_index
        """
        mask_match, mask_match_len = self._resolve_mask_match(mask)
        r = self.vapi.classify_add_del_table(
            is_add=1,
            mask=mask_match,
            mask_len=mask_match_len,
            match_n_vectors=(len(mask) - 1) // 32 + 1,
            miss_next_index=0,
            current_data_flag=1,
            current_data_offset=data_offset,
            next_table_index=next_table_index,
        )
        self.assertIsNotNone(r, "No response msg for add_del_table")
        self.acl_tbl_idx[key] = r.new_table_index

    def create_classify_session(
        self, table_index, match, pbr_option=0, vrfid=0, is_add=1
    ):
        """Create Classify Session

        :param int table_index: table index to identify classify table.
        :param str match: matched value for interested traffic.
        :param int pbr_option: enable/disable PBR feature.
        :param int vrfid: VRF id.
        :param int is_add: option to configure classify session.
            - create(1) or delete(0)
        """
        mask_match, mask_match_len = self._resolve_mask_match(match)
        r = self.vapi.classify_add_del_session(
            is_add=is_add,
            table_index=table_index,
            match=mask_match,
            match_len=mask_match_len,
            opaque_index=0,
            action=pbr_option,
            metadata=vrfid,
        )
        self.assertIsNotNone(r, "No response msg for add_del_session")

    def input_acl_set_interface(self, intf, table_index, is_add=1):
        """Configure Input ACL interface

        :param VppInterface intf: Interface to apply Input ACL feature.
        :param int table_index: table index to identify classify table.
        :param int is_add: option to configure classify session.
            - enable(1) or disable(0)
        """
        r = None
        if self.af == AF_INET:
            r = self.vapi.input_acl_set_interface(
                is_add, intf.sw_if_index, ip4_table_index=table_index
            )
        elif self.af == AF_INET6:
            r = self.vapi.input_acl_set_interface(
                is_add, intf.sw_if_index, ip6_table_index=table_index
            )
        else:
            r = self.vapi.input_acl_set_interface(
                is_add, intf.sw_if_index, l2_table_index=table_index
            )
        self.assertIsNotNone(r, "No response msg for acl_set_interface")

    def output_acl_set_interface(self, intf, table_index, is_add=1):
        """Configure Output ACL interface

        :param VppInterface intf: Interface to apply Output ACL feature.
        :param int table_index: table index to identify classify table.
        :param int is_add: option to configure classify session.
            - enable(1) or disable(0)
        """
        r = None
        if self.af == AF_INET:
            r = self.vapi.output_acl_set_interface(
                is_add, intf.sw_if_index, ip4_table_index=table_index
            )
        elif self.af == AF_INET6:
            r = self.vapi.output_acl_set_interface(
                is_add, intf.sw_if_index, ip6_table_index=table_index
            )
        else:
            r = self.vapi.output_acl_set_interface(
                is_add, intf.sw_if_index, l2_table_index=table_index
            )
        self.assertIsNotNone(r, "No response msg for acl_set_interface")

    def config_pbr_fib_entry(self, intf, is_add=1):
        """Configure fib entry to route traffic toward PBR VRF table

        :param VppInterface intf: destination interface to be routed for PBR.

        """
        addr_len = 24
        self.vapi.ip_add_del_route(
            dst_address=intf.local_ip4,
            dst_address_length=addr_len,
            next_hop_address=intf.remote_ip4,
            table_id=self.pbr_vrfid,
            is_add=is_add,
        )

    def verify_vrf(self, vrf_id):
        """
        Check if the FIB table / VRF ID is configured.

        :param int vrf_id: The FIB table / VRF ID to be verified.
        :return: 1 if the FIB table / VRF ID is configured, otherwise return 0.
        """
        ip_fib_dump = self.vapi.ip_route_dump(vrf_id, False)
        vrf_count = len(ip_fib_dump)
        if vrf_count == 0:
            self.logger.info("IPv4 VRF ID %d is not configured" % vrf_id)
            return 0
        else:
            self.logger.info("IPv4 VRF ID %d is configured" % vrf_id)
            return 1