aboutsummaryrefslogtreecommitdiffstats
path: root/test/template_classifier.py
blob: ec2a4143eecc394c332ef25f8ecee7258198217a (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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144<
/*
 * Copyright (c) 2015-2016 Cisco and/or its affiliates.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at:
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

vl_api_version 1.0.0

/** \brief DHCP Proxy config add / del request
    @param client_index - opaque cookie to identify the sender
    @param context - sender context, to match reply w/ request
    @param rx_vrf_id - Rx/interface vrf id
    @param server_vrf_id - server vrf id
    @param if_ipv6 - ipv6 if non-zero, else ipv4
    @param is_add - add the config if non-zero, else delete
    @param insert_circuit_id - option82 suboption 1 fib number
    @param dhcp_server[] - server address
    @param dhcp_src_address[] - <fix this, need details>
*/
autoreply define dhcp_proxy_config
{
  u32 client_index;
  u32 context;
  u32 rx_vrf_id;
  u32 server_vrf_id;
  u8 is_ipv6;
  u8 is_add;
  u8 dhcp_server[16];
  u8 dhcp_src_address[16];
};

/** \brief DHCP Proxy set / unset vss request
    @param client_index - opaque cookie to identify the sender
    @param context - sender context, to match reply w/ request
    @param tbl_id - table id
    @param oui - first part of vpn id
    @param fib_id - second part of vpn id
    @param is_ipv6 - ip6 if non-zero, else ip4
    @param is_add - set vss if non-zero, else delete
*/
autoreply define dhcp_proxy_set_vss
{
  u32 client_index;
  u32 context;
  u32 tbl_id;
  u32 oui;
  u32 fib_id;
  u8 is_ipv6;
  u8 is_add;
};

/** \brief DHCP Client config add / del request
    @param client_index - opaque cookie to identify the sender
    @param context - sender context, to match reply w/ request
    @param sw_if_index - index of the interface for DHCP client
    @param hostname - hostname
    @param client_id - Client ID - option 61
    @param is_add - add the config if non-zero, else delete
    @param want_dhcp_event - DHCP event sent to the sender
           via dhcp_compl_event API message if non-zero
    @param pid - sender's pid
*/
autoreply define dhcp_client_config
{
  u32 client_index;
  u32 context;
  u32 sw_if_index;
  u8 hostname[64];
  u8 client_id[64];
  u8 is_add;
  u8 want_dhcp_event;
  u32 pid;
};

/** \brief Tell client about a DHCP completion event
    @param client_index - opaque cookie to identify the sender
    @param pid - client pid registered to receive notification
    @param is_ipv6 - if non-zero the address is ipv6, else ipv4
    @param mask_width - The length of the subnet mask assigned
    @param host_address - Host IP address
    @param router_add
#!/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