-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
James Bensley
committed
Jan 17, 2025
1 parent
c3b0883
commit 11f151f
Showing
10 changed files
with
890 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
.DS_Store | ||
*.egg-info | ||
.idea | ||
.mypy_cache | ||
.pytest_cache/ | ||
__pycache__ | ||
README.html | ||
.tox | ||
venv | ||
.venv | ||
.vscode |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
# NETwork Entropy Tester | ||
|
||
NETwork Entropy Tester can be used to generate a stream of packets with changing entropy. | ||
|
||
When sending traffic out of a single interface, NET will increment in every packet some piece of header entropy as specified by the CLI args. For example, to test how a network device load-balances traffic by hashing on various packet header fields, NET can be used to send traffic out of a single interface, into a router, and the router may then forward that traffic onwards over a LAG or ECMP path. | ||
|
||
When sending traffic out of more than one interface, NET still increments the header entropy data in every packet, but it also performs per-packet round-robin transmission of the packets across all interfaces. | ||
|
||
```text | ||
ECMP LAG ECMP | ||
┌────────┐ ─────► ┌─────────┐ ─────► ┌─────────┐ ─────► ┌──────┐ | ||
│ NET Tx │ ─────► │ Router1 │ ─────► │ Router2 │ ─────► │ Sink │ | ||
└────────┘ ─────► └─────────┘ ─────► └─────────┘ ─────► └──────┘ | ||
``` | ||
|
||
## Features | ||
|
||
```shell | ||
$ python3 ./net.py -h | ||
usage: net.py [-h] [-d D] [-g G] -i I [-s] [-p] [--l2-dst] [--l2-src] [--l2-inner] [--dst-mac DST_MAC] [--src-mac SRC_MAC] [-m] [--mpls-label] [-6] [--l3-dst] | ||
[--l3-src] [--dst-ipv4 DST_IPV4] [--src-ipv4 SRC_IPV4] [--dst-ipv6 DST_IPV6] [--src-ipv6 SRC_IPV6] [-u] [--l4-dst] [--l4-src] | ||
|
||
Net Entropy Tester - Send packets with changing entropy | ||
|
||
options: | ||
-h, --help show this help message and exit | ||
-d D Duration to transmit for in seconds. (default: 10) | ||
-g G Inter-packet gap in seconds. Must be >= 0.0 and <= 60.0. Anything higher than 0.0 is reducing the pps rate. (default: 0.0) | ||
-i I Interface(s) to transmit on. This can be specified multiple times to round-robin packets across multiple interfaces. (default: []) | ||
-s Print stats during test (lowers pps rate). (default: False) | ||
-p Print the protocol stack which is being sent. (default: False) | ||
|
||
Ethernet Settings: | ||
--l2-dst Change the inner most destination MAC address per-frame. (default: False) | ||
--l2-src Change the inner most source MAC address per-frame. (default: False) | ||
--l2-inner Add an inner Ethernet header after the MPLS label(s) stack. This will automatically insert the Pseudowire Control-World. Requires -m at least | ||
once. (default: False) | ||
--dst-mac DST_MAC Set the initial destination MAC. (default: 00:00:00:00:00:02) | ||
--src-mac SRC_MAC Set the initial source MAC. (default: 00:00:00:00:00:01) | ||
|
||
MPLS Settings: | ||
-m Insert an MPLS label after the outer Ethernet header. Specify -m multiple times to stack multiple MPLS labels. (default: None) | ||
--mpls-label Change the inner most MPLS label per-frame. (default: False) | ||
|
||
L3 Settings: | ||
-6 Use IPv6 instead of IPv4. (default: False) | ||
--l3-dst Change the destination IP address per-packet. (default: False) | ||
--l3-src Change the source IP address per-packet. (default: False) | ||
--dst-ipv4 DST_IPV4 Set the initial destination IPv4 address. (default: 10.201.201.2) | ||
--src-ipv4 SRC_IPV4 Set the initial source IPv4 address. (default: 10.201.201.1) | ||
--dst-ipv6 DST_IPV6 Set the initial destination IPv6 address. (default: FD00::0201:2) | ||
--src-ipv6 SRC_IPV6 Set the initial source IPv6 address (default: FD00::0201:1) | ||
|
||
L4 Settings: | ||
-u Use UDP instead of TCP. (default: False) | ||
--l4-dst Change the destination port per-datagram. (default: False) | ||
--l4-src Change the source port per-datagram. (default: False) | ||
``` | ||
|
||
## Install | ||
|
||
```shell | ||
python3 -m venv --without-pip .venv && source .venv/bin/activate | ||
python3 -m ensurepip | ||
python3 -m pip install -r requirements.txt | ||
python3 -m pip freeze -l | ||
./net.py -h | ||
``` | ||
|
||
You need to run net.py as root in order to use raw sockets. When using sudo a different Python interpreter is used than the one in the venv you just set up, which will be missing the dependencies, therefore you use `sudo -E $(which python3) ./net.py` throughout this README. This is not needed if you are NOT using a venv or running in Docker. | ||
|
||
## Example | ||
|
||
```shell | ||
# Create two veth pairs we can load-balance traffic over | ||
sudo ip netns add NS1 && \ | ||
sudo ip netns add NS2 && \ | ||
sudo ip link add veth0 type veth peer name veth1 && \ | ||
sudo ip link add veth2 type veth peer name veth3 && \ | ||
sudo ip link set veth1 netns NS1 && \ | ||
sudo ip link set veth3 netns NS2 && \ | ||
sudo ip netns exec NS1 ip link set up dev veth1 && \ | ||
sudo ip netns exec NS2 ip link set up dev veth3 && \ | ||
sudo sysctl -w net.ipv6.conf.veth0.disable_ipv6=1 && \ | ||
sudo ip netns exec NS1 sysctl -w net.ipv6.conf.veth1.disable_ipv6=1 && \ | ||
sudo sysctl -w net.ipv6.conf.veth2.disable_ipv6=1 && \ | ||
sudo ip netns exec NS2 sysctl -w net.ipv6.conf.veth3.disable_ipv6=1 && \ | ||
sudo ip link set up dev veth0 && \ | ||
sudo ip link set up dev veth2 | ||
|
||
# Send traffic over both interfaces for 1 second with a rotating source IP address: | ||
$ sudo -E $(which python3) ./net.py -i veth0 -i veth2 --l3-src -d 1 | ||
Going to transmit for 1 seconds using interface(s) ['veth0', 'veth2'] | ||
|
||
Starting at 2024-02-07 17:41:42.335506 | ||
Finished at 2024-02-07 17:41:43.414516 | ||
Sent 4814 packets | ||
|
||
# Using tcpdump on one interface, we see the odd numbered source IPs: | ||
$ sudo tcpdump -i veth0 -c 3 -t 2>/dev/null | ||
IP 10.201.201.1.1024 > 10.201.201.2.1024: Flags [S], seq 0, win 8192, length 0 | ||
IP 10.201.201.3.1024 > 10.201.201.2.1024: Flags [S], seq 0, win 8192, length 0 | ||
IP 10.201.201.5.1024 > 10.201.201.2.1024: Flags [S], seq 0, win 8192, length 0 | ||
|
||
# Using tcpdump on the other interface we see the even numbered source IPs: | ||
$ sudo tcpdump -i veth2 -c 3 -t 2>/dev/null | ||
IP 10.201.201.2.1024 > 10.201.201.2.1024: Flags [S], seq 0, win 8192, length 0 | ||
IP 10.201.201.4.1024 > 10.201.201.2.1024: Flags [S], seq 0, win 8192, length 0 | ||
IP 10.201.201.6.1024 > 10.201.201.2.1024: Flags [S], seq 0, win 8192, length 0 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,265 @@ | ||
from __future__ import annotations | ||
|
||
import argparse | ||
import ipaddress | ||
from typing import Any | ||
|
||
from settings import Settings | ||
|
||
|
||
class CliArgs: | ||
""" | ||
Generate and parse CLI args, updating the application settings with the | ||
results | ||
""" | ||
|
||
@staticmethod | ||
def create_parser() -> argparse.ArgumentParser: | ||
""" | ||
Create the CLI parser with all desired CLI options | ||
""" | ||
parser = argparse.ArgumentParser( | ||
description="Net Entropy Tester - Send packets with changing entropy", | ||
formatter_class=argparse.ArgumentDefaultsHelpFormatter, | ||
) | ||
|
||
parser.add_argument( | ||
"-d", | ||
help="Duration to transmit for in seconds.", | ||
type=int, | ||
required=False, | ||
default=Settings.MAX_DURATION, | ||
) | ||
parser.add_argument( | ||
"-g", | ||
help="Inter-packet gap in seconds. Must be >= 0.0 and <= 60.0. " | ||
"Anything higher than 0.0 is reducing the pps rate.", | ||
type=float, | ||
required=False, | ||
default=Settings.INTER_PACKET_GAP, | ||
) | ||
parser.add_argument( | ||
"-i", | ||
help="Interface(s) to transmit on. This can be specified multiple " | ||
"times to round-robin packets across multiple interfaces.", | ||
type=str, | ||
required=True, | ||
action='append', | ||
default=Settings.INTERFACES, | ||
) | ||
parser.add_argument( | ||
"-s", | ||
help="Print stats during test (lowers pps rate).", | ||
action="store_true", | ||
required=False, | ||
default=Settings.RUNNING_STATS, | ||
) | ||
parser.add_argument( | ||
"-p", | ||
help="Print the protocol stack which is being sent.", | ||
action="store_true", | ||
required=False, | ||
default=Settings.PRINT_PACKET, | ||
) | ||
|
||
eth_args = parser.add_argument_group("Ethernet Settings") | ||
eth_args.add_argument( | ||
"--l2-dst", | ||
help="Change the inner most destination MAC address per-frame.", | ||
default=False, | ||
action="store_true", | ||
required=False, | ||
) | ||
eth_args.add_argument( | ||
"--l2-src", | ||
help="Change the inner most source MAC address per-frame.", | ||
default=False, | ||
action="store_true", | ||
required=False, | ||
) | ||
eth_args.add_argument( | ||
"--l2-inner", | ||
help="Add an inner Ethernet header after the MPLS label(s) stack. " | ||
"This will automatically insert the Pseudowire Control-World. " | ||
"Requires -m at least once.", | ||
default=False, | ||
action="store_true", | ||
required=False, | ||
) | ||
eth_args.add_argument( | ||
"--dst-mac", | ||
help=f"Set the initial destination MAC.", | ||
default=Settings.ETHERNET_DST, | ||
type=str, | ||
required=False, | ||
) | ||
eth_args.add_argument( | ||
"--src-mac", | ||
help=f"Set the initial source MAC.", | ||
default=Settings.ETHERNET_SRC, | ||
type=str, | ||
required=False, | ||
) | ||
|
||
mpls_args = parser.add_argument_group("MPLS Settings") | ||
mpls_args.add_argument( | ||
"-m", | ||
help="Insert an MPLS label after the outer Ethernet header. " | ||
"Specify -m multiple times to stack multiple MPLS labels.", | ||
default=None, | ||
action="count", | ||
required=False, | ||
) | ||
mpls_args.add_argument( | ||
"--mpls-label", | ||
help="Change the inner most MPLS label per-frame.", | ||
default=False, | ||
action="store_true", | ||
required=False, | ||
) | ||
|
||
ip_args = parser.add_argument_group("L3 Settings") | ||
ip_args.add_argument( | ||
"-6", | ||
help="Use IPv6 instead of IPv4.", | ||
default=False, | ||
action="store_true", | ||
required=False, | ||
) | ||
ip_args.add_argument( | ||
"--l3-dst", | ||
help="Change the destination IP address per-packet.", | ||
default=False, | ||
action="store_true", | ||
required=False, | ||
) | ||
ip_args.add_argument( | ||
"--l3-src", | ||
help="Change the source IP address per-packet.", | ||
default=False, | ||
action="store_true", | ||
required=False, | ||
) | ||
ip_args.add_argument( | ||
"--dst-ipv4", | ||
help=f"Set the initial destination IPv4 address.", | ||
default=Settings.IPV4_DST, | ||
type=str, | ||
required=False, | ||
) | ||
ip_args.add_argument( | ||
"--src-ipv4", | ||
help=f"Set the initial source IPv4 address.", | ||
default=Settings.IPV4_SRC, | ||
type=str, | ||
required=False, | ||
) | ||
ip_args.add_argument( | ||
"--dst-ipv6", | ||
help=f"Set the initial destination IPv6 address.", | ||
default=Settings.IPV6_DST, | ||
type=str, | ||
required=False, | ||
) | ||
ip_args.add_argument( | ||
"--src-ipv6", | ||
help=f"Set the initial source IPv6 address", | ||
default=Settings.IPV6_SRC, | ||
type=str, | ||
required=False, | ||
) | ||
|
||
tcp_args = parser.add_argument_group("L4 Settings") | ||
tcp_args.add_argument( | ||
"-u", | ||
help="Use UDP instead of TCP.", | ||
default=False, | ||
action="store_true", | ||
required=False, | ||
) | ||
tcp_args.add_argument( | ||
"--l4-dst", | ||
help="Change the destination port per-datagram.", | ||
default=False, | ||
action="store_true", | ||
required=False, | ||
) | ||
tcp_args.add_argument( | ||
"--l4-src", | ||
help="Change the source port per-datagram.", | ||
default=False, | ||
action="store_true", | ||
required=False, | ||
) | ||
|
||
return parser | ||
|
||
@staticmethod | ||
def parse_cli_args() -> dict[str, Any]: | ||
""" | ||
Parse the CLI args and update the settings | ||
""" | ||
parser = CliArgs.create_parser() | ||
args = vars(parser.parse_args()) | ||
|
||
if args["g"] < 0.0 or args["g"] > 60.0: | ||
raise ValueError(f"-g must be >= 0.0 and <= 60.0, not {args['g']}") | ||
|
||
if args["mpls_label"] and not args["m"]: | ||
raise ValueError(f"--mpls-label requires -m") | ||
|
||
if args["l2_inner"] and not args["m"]: | ||
raise ValueError(f"--l2-inner requires -m") | ||
|
||
assert ( | ||
type(ipaddress.ip_address(args["dst_ipv4"])) | ||
== ipaddress.IPv4Address | ||
) | ||
assert ( | ||
type(ipaddress.ip_address(args["src_ipv4"])) | ||
== ipaddress.IPv4Address | ||
) | ||
assert ( | ||
type(ipaddress.ip_address(args["dst_ipv6"])) | ||
== ipaddress.IPv6Address | ||
) | ||
assert ( | ||
type(ipaddress.ip_address(args["src_ipv6"])) | ||
== ipaddress.IPv6Address | ||
) | ||
|
||
Settings.MAX_DURATION = args["d"] | ||
Settings.INTER_PACKET_GAP = args["g"] | ||
Settings.INTERFACES = args["i"] | ||
Settings.RUNNING_STATS = args["s"] | ||
Settings.PRINT_PACKET = args["p"] | ||
Settings.ETHERNET_DST_ROTATE = args["l2_dst"] | ||
Settings.ETHERNET_SRC_ROTATE = args["l2_src"] | ||
Settings.ETHERNET_DST = args["dst_mac"] | ||
Settings.ETHERNET_SRC = args["src_mac"] | ||
Settings.ETHERNET_INNER = args["l2_inner"] | ||
Settings.MPLS = args["m"] | ||
Settings.MPLS_ROTATE = args["mpls_label"] | ||
Settings.IP_DST_ROTATE = args["l3_dst"] | ||
Settings.IP_SRC_ROTATE = args["l3_src"] | ||
Settings.IPV4_DST = args["dst_ipv4"] | ||
Settings.IPV4_SRC = args["src_ipv4"] | ||
Settings.IPV6 = args["6"] | ||
Settings.IPV6_DST = args["dst_ipv6"] | ||
Settings.IPV6_SRC = args["src_ipv6"] | ||
Settings.L4_DST_ROTATE = args["l4_dst"] | ||
Settings.L4_SRC_ROTATE = args["l4_src"] | ||
Settings.UDP = args["u"] | ||
|
||
if ( | ||
Settings.ETHERNET_DST_ROTATE | ||
or Settings.ETHERNET_SRC_ROTATE | ||
or Settings.MPLS_ROTATE | ||
or Settings.IP_DST_ROTATE | ||
or Settings.IP_SRC_ROTATE | ||
or Settings.L4_DST_ROTATE | ||
or Settings.L4_SRC_ROTATE | ||
): | ||
Settings.ROTATE = True | ||
|
||
return args |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
#!/usr/bin/env python3 | ||
|
||
from __future__ import annotations | ||
|
||
from cli import CliArgs | ||
from tx import Tx | ||
|
||
CliArgs.parse_cli_args() | ||
Tx.run() |
Oops, something went wrong.