|
| 1 | +# Dear snappi, please meet Scapy! |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +As [Scapy Project](https://scapy.net/) puts it: |
| 6 | + |
| 7 | +> Scapy is a powerful interactive packet manipulation program. It is able to forge or decode packets of a wide number of protocols, send them on the wire, capture them, match requests and replies, and much more. |
| 8 | +
|
| 9 | +In other words, Scapy allows you to craft any packet you want, including L2-4 headers as well as L7 payload. It can also send these packets into a network, as is. |
| 10 | + |
| 11 | +Meanwhile, the [Open Traffic Generator API](https://otg.dev) with its Python client library [snappi](https://snappi.dev), is really great with scaling up the task of putting the packets onto a wire by leveraging OTG-compliant traffic generators, like [Ixia-c](https://ixia-c.dev). The OTG supports the notion of flows, with precise capabilities to schedule packet transmission - rate, interval, duration. It also has rich capabilities to iterate over ranges of MAC and IP addresses, TCP/UDP ports and other parameters. |
| 12 | + |
| 13 | +Wouldn't it be nice if these two could meet and work as a team? |
| 14 | + |
| 15 | +## How would it work? |
| 16 | + |
| 17 | + |
| 18 | + |
| 19 | +Let's assume you want to stress-test a network device with a large number of specific packets. For example, DNS requests & replies. With Scapy, it is easy to craft such payload. Note, how Scapy allows you to create a subset of packet layers. In this case, we're skipping Ethernet, IP and UDP, as OTG would take care of them. |
| 20 | + |
| 21 | +```Python |
| 22 | +from scapy.all import * |
| 23 | + |
| 24 | +# create custom DNS request payloads with Scapy |
| 25 | +requests = [DNS(id=0, rd=1, qr=0, qd=DNSQR(qtype="A", qname="example.com")), |
| 26 | + DNS(id=1, rd=1, qr=0, qd=DNSQR(qtype="AAAA", qname="example.com"))] |
| 27 | +``` |
| 28 | + |
| 29 | +Now, with snappi, we can use these payloads to create a dedicated flow for each Scapy packet, with duration and rate we need. |
| 30 | + |
| 31 | +```Python |
| 32 | +import snappi |
| 33 | + |
| 34 | +api = snappi.api(location=OTG_API, verify=False) |
| 35 | +cfg = api.config() |
| 36 | +packet_count = 10 # send 10 packets per each flow |
| 37 | + |
| 38 | +# flows for requests |
| 39 | +for i in range(len(requests)): |
| 40 | + n = "request" + str(i) |
| 41 | + f = cfg.flows.flow(name=n)[-1] |
| 42 | + # will use UDP with custom payload |
| 43 | + eth, ip, udp, payload = f.packet.ethernet().ipv4().udp().custom() |
| 44 | + eth.src.value, eth.dst.value = "02:00:00:00:01:AA", "02:00:00:00:02:AA" |
| 45 | + ip.src.value, ip.dst.value = "192.0.2.1", "192.0.2.2" |
| 46 | + # increment UDP source port number for each packet |
| 47 | + udp.src_port.increment.start = 1024 |
| 48 | + udp.src_port.increment.step = 1 |
| 49 | + udp.src_port.increment.count = requests_count |
| 50 | + udp.dst_port.value = 53 |
| 51 | + # copy a payload from Scapy packet into a snappi flow |
| 52 | + payload.bytes = requests[i].build().hex() |
| 53 | + # number of packets to transmit |
| 54 | + f.duration.fixed_packets.packets = requests_count |
| 55 | + # delay between flows to simulate a sequence of packets: 1ms |
| 56 | + f.duration.fixed_packets.delay.microseconds = 1000 * i |
| 57 | +``` |
| 58 | + |
| 59 | + Some details above are omitted, see [scapy2otg.py](scapy2otg.py) for more. |
| 60 | + |
| 61 | + As a result, the produced OTG configuration of the first flow of the DNS requests will have a custom payload after the UDP layer (see the very end of the YAML below): |
| 62 | + |
| 63 | + ```Yaml |
| 64 | +flows: |
| 65 | +- duration: |
| 66 | + choice: fixed_packets |
| 67 | + fixed_packets: |
| 68 | + delay: |
| 69 | + choice: microseconds |
| 70 | + microseconds: 0 |
| 71 | + gap: 12 |
| 72 | + packets: 10 |
| 73 | + metrics: |
| 74 | + enable: true |
| 75 | +name: request0 |
| 76 | + packet: |
| 77 | + - choice: ethernet |
| 78 | + ethernet: |
| 79 | + dst: |
| 80 | + choice: value |
| 81 | + value: 02:00:00:00:02:AA |
| 82 | + src: |
| 83 | + choice: value |
| 84 | + value: 02:00:00:00:01:AA |
| 85 | + - choice: ipv4 |
| 86 | + ipv4: |
| 87 | + dst: |
| 88 | + choice: value |
| 89 | + value: 192.0.2.2 |
| 90 | + src: |
| 91 | + choice: value |
| 92 | + value: 192.0.2.1 |
| 93 | + - choice: udp |
| 94 | + udp: |
| 95 | + dst_port: |
| 96 | + choice: value |
| 97 | + value: 53 |
| 98 | + src_port: |
| 99 | + choice: increment |
| 100 | + increment: |
| 101 | + count: 10 |
| 102 | + start: 1024 |
| 103 | + step: 1 |
| 104 | + - choice: custom |
| 105 | + custom: |
| 106 | + bytes: 000001000001000000000000076578616d706c6503636f6d0000010001 |
| 107 | +``` |
| 108 | +
|
| 109 | +## Captured & Framed |
| 110 | +
|
| 111 | +When captured, the packet frames generated by Ixia-c look as DNS queries in Wireshark, with the exception of additional data signature Ixia-c adds at the end of each packet. The signature is needed to identify each packet at the receiving side, and measure latency, packet loss and other metrics. If you look into [scapy2otg.py](scapy2otg.py), the following line instructs Ixia-c to add the signature: `f.metrics.enable = True`. |
| 112 | + |
| 113 | + |
| 114 | + |
| 115 | +An alternative implementation that uses port-level metrics instead of packet signatures can be found in [scapy2otg-port.py](scapy2otg-port.py) |
| 116 | + |
| 117 | +## Giving it a try |
| 118 | + |
| 119 | +### Prerequisites |
| 120 | + |
| 121 | +* Linux host or VM with sudo permissions and Docker support. See [some ready-to-use options](README.md#options-for-linux-vm-deployment-for-containerlab) |
| 122 | +* `git` - how to install depends on your Linux distro. |
| 123 | +* [Docker](https://docs.docker.com/engine/install/) |
| 124 | +* [Containerlab](https://containerlab.dev/install/) |
| 125 | +* Clone of the repository |
| 126 | + |
| 127 | + ```Shell |
| 128 | + git clone --recurse-submodules https://github.com/open-traffic-generator/otg-examples.git |
| 129 | + cd otg-examples/clab/ixia-c-b2b |
| 130 | + ``` |
| 131 | +### TLDR version |
| 132 | + |
| 133 | +```Shell |
| 134 | +make install build deploy run-scapy clean |
| 135 | +``` |
| 136 | + |
| 137 | +Open `p1.pcap` and `p2.pcap` to inspect captured packets. |
| 138 | + |
| 139 | +Otherwise, follow a step-by-step guide: |
| 140 | + |
| 141 | +### Prepare a `snappi` container image |
| 142 | + |
| 143 | +Run the following only once, to build a container image where `snappi` program will execute: |
| 144 | + |
| 145 | +```Shell |
| 146 | +sudo docker build -t snappi:local . |
| 147 | +``` |
| 148 | + |
| 149 | +### Deploy a lab |
| 150 | + |
| 151 | +```Shell |
| 152 | +sudo -E containerlab deploy -t topo.yml |
| 153 | +``` |
| 154 | + |
| 155 | +### Run scapy2otg test |
| 156 | + |
| 157 | +```Shell |
| 158 | +sudo docker exec -it clab-ixcb2b-snappi bash -c "OTG_API='https://clab-ixcb2b-ixia-c:8443' OTG_LOCATION_P1=eth1 OTG_LOCATION_P2=eth2 python scapy2otg.py" |
| 159 | +``` |
| 160 | + |
| 161 | +### Destroy the lab |
| 162 | + |
| 163 | +```Shell |
| 164 | +sudo -E containerlab destroy -t topo.yml |
| 165 | +``` |
| 166 | + |
0 commit comments