Skip to content

Commit

Permalink
Initial push to GitHub
Browse files Browse the repository at this point in the history
  • Loading branch information
James Bensley committed Jan 17, 2025
1 parent c3b0883 commit 11f151f
Show file tree
Hide file tree
Showing 10 changed files with 890 additions and 0 deletions.
11 changes: 11 additions & 0 deletions .gitignore
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
110 changes: 110 additions & 0 deletions README.md
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
```
265 changes: 265 additions & 0 deletions cli.py
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
9 changes: 9 additions & 0 deletions net.py
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()
Loading

0 comments on commit 11f151f

Please sign in to comment.