| #!/usr/bin/env python |
| |
| import unittest |
| import socket |
| import binascii |
| import time |
| |
| from framework import VppTestCase, VppTestRunner |
| |
| from scapy.packet import Raw |
| from scapy.layers.l2 import Ether |
| from scapy.layers.inet import IP, UDP |
| from scapy.utils import hexdump |
| from util import ppp |
| |
| class TestFlowperpkt(VppTestCase): |
| """ Flow-per-packet plugin: test both L2 and IP4 reporting """ |
| |
| def setUp(self): |
| """ |
| Set up |
| |
| **Config:** |
| - create three PG interfaces |
| - create a couple of loopback interfaces |
| """ |
| super(TestFlowperpkt, self).setUp() |
| |
| self.create_pg_interfaces(range(3)) |
| |
| self.pg_if_packet_sizes = [150] |
| |
| self.interfaces = list(self.pg_interfaces) |
| |
| for intf in self.interfaces: |
| intf.admin_up() |
| intf.config_ip4() |
| intf.resolve_arp() |
| |
| def tearDown(self): |
| """Run standard test teardown""" |
| super(TestFlowperpkt, self).tearDown() |
| |
| |
| def create_stream(self, src_if, dst_if, packet_sizes): |
| """Create a packet stream to tickle the plugin |
| |
| :param VppInterface src_if: Source interface for packet stream |
| :param VppInterface src_if: Dst interface for packet stream |
| :param list packet_sizes: Sizes to test |
| """ |
| pkts = [] |
| for size in packet_sizes: |
| info = self.create_packet_info(src_if.sw_if_index, |
| dst_if.sw_if_index) |
| payload = self.info_to_payload(info) |
| p = (Ether(src=src_if.local_mac, dst=dst_if.remote_mac) / |
| IP(src=src_if.remote_ip4, dst=dst_if.remote_ip4) / |
| UDP(sport=1234, dport=4321) / |
| Raw(payload)) |
| info.data = p.copy() |
| self.extend_packet(p, size) |
| pkts.append(p) |
| return pkts |
| |
| def verify_ipfix(self, collector_if): |
| """Check the ipfix capture""" |
| found_data_packet = 0 |
| found_template_packet = 0 |
| found_l2_data_packet = 0 |
| found_l2_template_packet = 0 |
| |
| # Scapy, of course, understands ipfix not at all... |
| # These data vetted by manual inspection in wireshark |
| # X'ed out fields are timestamps, which will absolutely |
| # fail to compare. At L2, kill the pg src MAC address, which |
| # is random. |
| |
| data_udp_string = "1283128300370000000a002fXXXXXXXX00000000000000010100001f0000000100000002ac100102ac10020200XXXXXXXXXXXXXXXX0092" |
| |
| template_udp_string = "12831283003c0000000a0034XXXXXXXX00000002000000010002002401000007000a0004000e000400080004000c000400050001009c000801380002" |
| |
| l2_data_udp_string = "12831283003c0000000a0034XXXXXXXX0000000100000001010100240000000100000002XXXXXXXXXXXX02020000ff020008XXXXXXXXXXXXXXXX0092" |
| |
| l2_template_udp_string = "12831283003c0000000a0034XXXXXXXX00000002000000010002002401010007000a0004000e0004003800060050000601000002009c000801380002" |
| |
| cap_x = "X" |
| data_udp_len = len(data_udp_string) |
| template_udp_len = len(template_udp_string) |
| l2_data_udp_len = len(l2_data_udp_string) |
| l2_template_udp_len = len(l2_template_udp_string) |
| |
| self.logger.info("Look for ipfix packets on %s sw_if_index %d " |
| % (collector_if.name, collector_if.sw_if_index)) |
| capture = collector_if.get_capture() |
| |
| for p in capture: |
| data_result = "" |
| template_result = "" |
| l2_data_result = "" |
| l2_template_result = "" |
| unmasked_result = "" |
| ip = p[IP] |
| udp = p[UDP] |
| self.logger.info("src %s dst %s" % (ip.src, ip.dst)) |
| self.logger.info(" udp src_port %s dst_port %s" |
| % (udp.sport, udp.dport)) |
| |
| # Hex-dump the UDP datagram 4 ways in parallel |
| # X'ing out incomparable fields |
| # Python completely bites at this sort of thing, of course |
| |
| x = str(udp) |
| l = len(x) |
| i = 0 |
| while i < l: |
| # If current index within range |
| if i < data_udp_len/2: |
| # See if we're supposed to don't care the data |
| if ord(data_udp_string[i*2]) == ord(cap_x[0]): |
| data_result = data_result + "XX" |
| else: |
| data_result = data_result + ("%02x" % ord(x[i])) |
| else: |
| # index out of range, emit actual data |
| # The test will fail, but it may help debug, etc. |
| data_result = data_result + ("%02x" % ord(x[i])) |
| |
| if i < template_udp_len/2: |
| if ord(template_udp_string[i*2]) == ord(cap_x[0]): |
| template_result = template_result + "XX" |
| else: |
| template_result = template_result + ("%02x" % ord(x[i])) |
| else: |
| template_result = template_result + ("%02x" % ord(x[i])) |
| |
| if i < l2_data_udp_len/2: |
| # See if we're supposed to don't care the data |
| if ord(l2_data_udp_string[i*2]) == ord(cap_x[0]): |
| l2_data_result = l2_data_result + "XX" |
| else: |
| l2_data_result = l2_data_result + ("%02x" % ord(x[i])) |
| else: |
| # index out of range, emit actual data |
| # The test will fail, but it may help debug, etc. |
| l2_data_result = l2_data_result + ("%02x" % ord(x[i])) |
| |
| if i < l2_template_udp_len/2: |
| if ord(l2_template_udp_string[i*2]) == ord(cap_x[0]): |
| l2_template_result = l2_template_result + "XX" |
| else: |
| l2_template_result = l2_template_result + ("%02x" % ord(x[i])) |
| else: |
| l2_template_result = l2_template_result + ("%02x" % ord(x[i])) |
| # In case we need to |
| unmasked_result = unmasked_result + ("%02x" % ord(x[i])) |
| |
| i = i + 1 |
| |
| if data_result == data_udp_string: |
| self.logger.info ("found ip4 data packet") |
| found_data_packet = 1 |
| elif template_result == template_udp_string: |
| self.logger.info ("found ip4 template packet") |
| found_template_packet = 1 |
| elif l2_data_result == l2_data_udp_string: |
| self.logger.info ("found l2 data packet") |
| found_l2_data_packet = 1 |
| elif l2_template_result == l2_template_udp_string: |
| self.logger.info ("found l2 template packet") |
| found_l2_template_packet = 1 |
| else: |
| self.logger.info ("unknown pkt '%s'" % unmasked_result) |
| |
| self.assertTrue (found_data_packet == 1) |
| self.assertTrue (found_template_packet == 1) |
| self.assertTrue (found_l2_data_packet == 1) |
| self.assertTrue (found_l2_template_packet == 1) |
| |
| def test_L3_fpp(self): |
| """ Flow per packet L3 test """ |
| |
| # Configure an ipfix report on the [nonexistent] collector |
| # 172.16.3.2, as if it was connected to the pg2 interface |
| # Install a FIB entry, so the exporter's work won't turn into |
| # an ARP request |
| |
| self.pg_enable_capture(self.pg_interfaces) |
| self.vapi.cli("set ip arp pg2 172.16.3.2 dead.beef.0002") |
| self.logger.info(self.vapi.cli("set ipfix exporter collector 172.16.3.2 src 172.16.3.1 path-mtu 1450 template-interval 1")) |
| |
| # Export flow records for all pkts transmitted on pg1 |
| |
| self.logger.info(self.vapi.cli("flowperpkt feature add-del pg1")) |
| self.logger.info(self.vapi.cli("flowperpkt feature add-del pg1 l2")) |
| |
| # Arrange to minimally trace generated ipfix packets |
| self.logger.info(self.vapi.cli("trace add flowperpkt-ipv4 10")) |
| self.logger.info(self.vapi.cli("trace add flowperpkt-l2 10")) |
| |
| # Create a stream from pg0 -> pg1, which causes |
| # an ipfix packet to be transmitted on pg2 |
| |
| pkts = self.create_stream(self.pg0, self.pg1, |
| self.pg_if_packet_sizes) |
| self.pg0.add_stream(pkts) |
| self.pg_start() |
| |
| # Flush the ipfix collector, so we don't need any |
| # asinine time.sleep(5) action |
| |
| self.logger.info(self.vapi.cli("ipfix flush")) |
| |
| # Make sure the 4 pkts we expect actually showed up |
| self.verify_ipfix(self.pg2) |
| |
| if __name__ == '__main__': |
| unittest.main(testRunner=VppTestRunner) |
| |
| |
| |
| |