scapyでトラフィックジェネレーター(笑)

## はじめに

ネットワーク評価の際にトラフィック負荷試験をする必要があり、簡易的なトラフィックジェネレーターソフトが必要でした。
せっかくなのでiperfよりも細かくパケットが定義できそうなscapyを使用して、簡易的なトラフィックジェネレーターを作成してみます。
スループットのパフォーマンスを向上させるため、scapyのsendpfast()を使用するのでtcpreplayもインストールします。
※ちなみに本スクリプトでは大量のショートパケットや1Gを超える帯域負荷は環境によって安定出力ができません(なので(笑))

## 環境の準備

### 本記事の環境

- Rocky Linux 9
  - 4core 8GB memory

```
--------------------      --------      ------------
| Rocky linux(src) | -1G- | L2SW | -1G- | receiver |
--------------------      --------      ------------
```

## 関連ソフトのインストール

### python

```shell
sudo dnf install -y python3.11

# インストールできたらvenvも作っておく
python3.11 -m venv .venv
```

### scapy

```shell
python3.11 -m pip install scapy
```

### tcpreplay

```shell
dnf install -y --enablerepo=epel tcpreplay
```

## 各プロトコルのトラフィック生成、送信

以下は送信したいプロトコル(TCP、UDP、ICMP)を選択し、記述したIPアドレス、ポート番号を使用して50000pps(およそ600Mbps)の負荷で送信し続けるコードです。
データヘッダには適当なバイト列を入れています。(ここでデータヘッダサイズを調整できます)
```python:traffgen.py
from scapy.all import *

def info():
    # IPアドレスとポートの設定
    iface = 'ens18' # 送信元インターフェースを指定
    src_ip = "192.168.1.18" # 送信元のIPアドレスを指定
    src_port = 10000 # 送信元ポート番号を指定

    dst_ip = "192.168.1.3" # 宛先のIPアドレスを指定
    dst_port = 80 # 宛先ポート番号を指定

    modeselect = input('select traffic gen mode: ')
    if modeselect == 'tcp':
        mode = tcppkt(src_ip,src_port,dst_ip,dst_port)
    elif modeselect == 'udp':
        mode = udppkt(src_ip,src_port,dst_ip,dst_port)
    elif modeselect == 'icmp':
        mode = icmppkt(src_ip,dst_ip)

    pkt = mode
    print(pkt)
    sendpfast(pkt, iface=iface, loop=1, file_cache=True, pps=50000)

def tcppkt(src_ip,src_port,dst_ip,dst_port):
    # 1パケット1500バイトのTCP SYNパケットを作成
    pkt = Ether()/\
        IP(src=src_ip, dst=dst_ip, flags="DF") /\
        TCP(sport=src_port, dport=dst_port, flags="S") /\
        (b"A" * 1460) # データヘッダサイズを指定
    return pkt

def udppkt(src_ip,src_port,dst_ip,dst_port):
    # 1パケット1500バイトのUDPパケットを作成
    pkt = Ether()/\
        IP(src=src_ip, dst=dst_ip, flags="DF") /\
        UDP(sport=src_port, dport=dst_port) /\
        (b"A" * 1472) # データヘッダサイズを指定
    return pkt

def icmppkt(src_ip,dst_ip):
    # 1パケット1500バイトのICMP echo requestパケットを作成
    pkt = Ether()/\
        IP(src=src_ip, dst=dst_ip, flags="DF") /\
        ICMP() /\
        (b"A" * 1472) # データヘッダサイズを指定
    return pkt

if __name__ == '__main__':
    info()
```

```shell
# venvに移行
[user@localhost traffgen]$ source .venv/bin/activate
(.venv) [user@localhost traffgen]$

# scapy使用時はroot権限で実行
(.venv) [user@localhost traffgen]$ sudo -s
[sudo] user のパスワード:
(.venv) [root@localhost traffgen]#

# traffgen.py実行(tcp or udp or icmpを入力)
(.venv) [root@localhost traffgen]# python3.11 traffgen.py 
select traffic gen mode: tcp
Ether / IP / TCP 192.168.1.18:ndmp > 192.168.1.3:http S / Raw
# パケットを送信し続けるため、停止したいタイミングでKeyboard interruptして停止します
```

- 受信側
  - TCP
  ![スクリーンショット 2025-01-30 225139.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2881599/11137eda-adc5-7964-01be-ad22f14d246a.png)
  ![スクリーンショット 2025-01-30 225252.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2881599/753145a0-8ab4-a28e-70e2-e0676809660e.png)

  - UDP
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2881599/ab1e140b-5d66-54fb-de6c-27cfe391f3f1.png)
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2881599/1d33f5af-0462-ae40-b17c-897cf00b3cec.png)

  - ICMP
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2881599/03676215-5839-c424-8743-10ae7363782b.png)
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2881599/85881521-2c6e-74d8-3e7d-4e0ecc9574c4.png)

TCPパケットでは安定した600Mbpsの受信ができておらず、波形にばらつきが見られます。
UDP・ICMPパケットではおおよそ綺麗な波形に見えます。

## pcapファイルを作成して送信

先程のコードでは、事前に定義したポート番号のみを使用してパケットを生成しますが、複数ポートの番号を使用することができません。
そこで、ポート1024-65535までを宛先としたパケットを格納したpcapファイルを生成します。ここでは送信元ポート番号も1024-65535からランダムな値を用いてみます。

```python:pcapgen.py
from scapy.all import *
import random

def info():
    src_ip = "192.168.1.18" # 送信元のIPアドレスを指定
    dst_ip = "192.168.1.3" # 宛先ポート番号を指定
    dst_port_range = range(1024,65535)

    pkts = []

    protocol = input('select protocol: ')
    if protocol == 'tcp':
        tcppcap(src_ip, dst_ip, dst_port_range, pkts)
    elif protocol == 'udp':
        udppcap(src_ip, dst_ip, dst_port_range, pkts)
    if protocol == 'icmp':
        icmppcap(src_ip, dst_ip, pkts)

def tcppcap(src_ip, dst_ip, dst_port_range, pkts):
    # 1パケット1500バイトのTCP SYNパケットを作成
    for dst_port in dst_port_range:
        src_port = random.randint(1024, 65535)
        pkt = Ether() /\
            IP(src=src_ip, dst=dst_ip, flags="DF") /\
            TCP(sport=src_port, dport=dst_port, flags="S") /\
            (b"A" * 1460)
        pkts.append(pkt)
    wrpcap('tcp_port_scan.pcap', pkts)
    print("TCP port scan pcap file was created successfully.")

def udppcap(src_ip, dst_ip, dst_port_range, pkts):
    # 1パケット1500バイトのUDPパケットを作成
    for dst_port in dst_port_range:
        src_port = random.randint(1024, 65535)
        pkt = Ether() /\
            IP(src=src_ip, dst=dst_ip, flags="DF") /\
            UDP(sport=src_port, dport=dst_port) /\
            (b"A" * 1472)
        pkts.append(pkt)
    wrpcap('udp_port_scan.pcap', pkts)
    print("UDP port scan pcap file was created successfully.")

def icmppcap(src_ip, dst_ip, pkts):
    # 1パケット1500バイトのICMPパケットを作成
    pkt = Ether() /\
        IP(src=src_ip, dst=dst_ip, flags="DF") /\
        ICMP() / (b"A" * 1472)
    pkts.append(pkt)
    wrpcap('icmp_port_scan.pcap', pkts)
    print("ICMP port scan pcap file was created successfully.")

if __name__ == '__main__':
    info()
```

以下のコードを実行し、pcapファイルからパケットを送信します。
```python:traffgen-pcap.py
from scapy.all import *

mode = input('select traffic gen mode: ')
if mode == 'tcp':
    pkt = rdpcap('tcp_port_scan.pcap')
if mode == 'udp':
    pkt = rdpcap('udp_port_scan.pcap')
if mode == 'icmp':
    pkt = rdpcap('icmp_port_scan.pcap')

print(pkt)

iface = 'ens18' # 送信元インターフェースを指定
sendpfast(pkt, iface=iface, loop=1, file_cache=True, pps=50000)
```

```shell
# 宛先port 1024-65535のpcapファイルを作成(tcp or udp or icmpを入力)
(.venv) [root@localhost traffgen]# python3.11 pcapgen.py 
select protocol: tcp
TCP port scan pcap file was created successfully.

# traffgen-pcap.py実行(tcp or udp or icmpを入力)
(.venv) [root@localhost traffgen]# python3.11 traffgen-pcap.py 
select traffic gen mode: tcp
<tcp_port_scan.pcap: TCP:64511 UDP:0 ICMP:0 Other:0>
```

- 受信側
  - TCP
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2881599/b7f46855-1bb9-15a1-1149-ab3e0982964c.png)
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2881599/3d56498a-c932-12ff-4cf3-d3a7f3ee1c7b.png)
  - UDP
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2881599/cecd295b-f6fa-b626-7355-1c50c96079bc.png)
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2881599/52b2daf1-1ba9-57e6-758d-9b74409446ad.png)

pcapファイルからパケットを送信した場合の方では、TCP・UDPどちらもおおよそ600Mbpsを安定して受信できており、こちらの方が安定した波形に見えます。

## その他

- sendpfast()のfile_cacheオプションはパケットをramにキャッシュする(らしい)ので、オプションを有効にするとスループットが安定する。
- mbpsオプションを使用していないのは、上記オプションを並装すると10～30秒程トラフィックを送信し続けると指定した値を超える過剰な負荷が発生した。
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2881599/bbe70f54-73ff-9a7b-6d79-cbdd62ff26ee.png)

  - ppsオプションの場合でも100秒程トラフィックを送信し続けた際に過剰な負荷が発生した。
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2881599/3ba4e713-2cfa-e6ba-94ff-10a9923e517d.png)

- pcapファイルを生成するならわざわざscapyからsendpfastを叩かずにtcpreplay+netmapすればさらに安定しそう。その検証はまた次回。

## 最後に

環境やシステム、設計によってネットワーク負荷試験の重要度は変わります。
特にトラフィックジェネレーターアプライアンスが必要でない今回のような試験の場面も多くあると思いますので、要件にマッチしたものをご使用いただければと思います。

また、本記事はTCP syn flood、UDP floodを行うスクリプトとなっているため、悪用厳禁でお願いいたします。