Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 144 additions & 9 deletions bench/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,34 @@ $(BASE_IMAGE):
.PHONY: base-image
base-image: $(BASE_IMAGE)

# Packages pre-baked into the cached base image. The bench bridges are
# `forward mode='none'` (no external NAT), so guest VMs cannot reach
# package mirrors at cloud-init time. virt-customize installs the union
# of tools every candidate / generator needs before the image is ever
# cloned. A sentinel file inside the image records that customisation
# ran, so re-runs are idempotent and don't double-install.
BASE_PACKAGES := nftables,tcpdump,iperf3,conntrack,iputils-ping,iputils-arping,jq,iproute2,python3
BASE_CUSTOM_MARK := /var/lib/thurward-bench.customised

$(BASE_IMAGE).customised: $(BASE_IMAGE)
@if ! command -v virt-customize >/dev/null 2>&1; then \
echo "ERROR: virt-customize not found (install guestfs-tools)"; exit 1; \
fi
@if sudo virt-cat -a $(BASE_IMAGE) $(BASE_CUSTOM_MARK) >/dev/null 2>&1; then \
echo " base image already customised"; \
else \
echo " customising base image with: $(BASE_PACKAGES)"; \
sudo virt-customize -a $(BASE_IMAGE) \
--install $(BASE_PACKAGES) \
--write '/etc/cloud/cloud.cfg.d/99-thurward-nocloud.cfg:datasource_list: [ NoCloud, None ]' \
--truncate /etc/machine-id \
--run-command 'mkdir -p /var/lib && date -u +%FT%TZ > $(BASE_CUSTOM_MARK)'; \
fi
@touch $@

.PHONY: base-customise base-customize
base-customise base-customize: $(BASE_IMAGE).customised

# ---------------------------------------------------------------------
# Networks
# ---------------------------------------------------------------------
Expand Down Expand Up @@ -139,20 +167,19 @@ net-down:

# Per-instance paths.
SUT_DISK = $(IMAGES_INST_DIR)/thur-sut-$(CAND).qcow2
SUT_SEED = $(IMAGES_INST_DIR)/thur-sut-$(CAND)-seed.iso
SUT_NAME = thur-sut-$(CAND)
SUT_DIR = $(BENCH_DIR)/candidates/$(CAND)
SUT_USER = bench

.PHONY: sub-up
sub-up: net-up $(BASE_IMAGE) $(HARNESS_KEY)
sub-up: net-up $(BASE_IMAGE).customised $(HARNESS_KEY)
@if [ ! -d $(SUT_DIR) ]; then echo "ERROR: $(SUT_DIR) does not exist (CAND=$(CAND))"; exit 1; fi
@mkdir -p $(IMAGES_INST_DIR)
@if [ ! -f $(SUT_DISK) ]; then \
echo "Cloning base image to instance: $(SUT_DISK)"; \
qemu-img create -f qcow2 -F qcow2 -b $(BASE_IMAGE) $(SUT_DISK) 20G; \
$(SCRIPTS_DIR)/render-cloud-init.sh $(CAND) sut $(SUT_DISK) $(HARNESS_KEY_PUB); \
fi
@$(SCRIPTS_DIR)/render-cloud-init.sh $(CAND) sut $(SUT_SEED) $(HARNESS_KEY_PUB)
@if ! virsh -c $(LIBVIRT_URI) dominfo $(SUT_NAME) >/dev/null 2>&1; then \
echo "Defining $(SUT_NAME)"; \
virt-install --connect $(LIBVIRT_URI) \
Expand All @@ -161,7 +188,6 @@ sub-up: net-up $(BASE_IMAGE) $(HARNESS_KEY)
--cpu host-passthrough \
--osinfo debian12 \
--disk path=$(SUT_DISK),format=qcow2,bus=virtio \
--disk path=$(SUT_SEED),device=cdrom,bus=sata \
--network network=lan-thurward,model=virtio \
--network network=wan-thurward,model=virtio \
--graphics none --console pty,target_type=serial \
Expand Down Expand Up @@ -193,24 +219,22 @@ sub-shell:
# ---------------------------------------------------------------------

GEN_DISK := $(IMAGES_INST_DIR)/thur-gen.qcow2
GEN_SEED := $(IMAGES_INST_DIR)/thur-gen-seed.iso
GEN_NAME := thur-gen

.PHONY: gen-up
gen-up: net-up $(BASE_IMAGE) $(HARNESS_KEY)
gen-up: net-up $(BASE_IMAGE).customised $(HARNESS_KEY)
@mkdir -p $(IMAGES_INST_DIR)
@if [ ! -f $(GEN_DISK) ]; then \
qemu-img create -f qcow2 -F qcow2 -b $(BASE_IMAGE) $(GEN_DISK) 20G; \
$(SCRIPTS_DIR)/render-cloud-init.sh trafficgen gen $(GEN_DISK) $(HARNESS_KEY_PUB); \
fi
@$(SCRIPTS_DIR)/render-cloud-init.sh trafficgen gen $(GEN_SEED) $(HARNESS_KEY_PUB)
@if ! virsh -c $(LIBVIRT_URI) dominfo $(GEN_NAME) >/dev/null 2>&1; then \
virt-install --connect $(LIBVIRT_URI) \
--name $(GEN_NAME) \
--memory 2048 --vcpus 1 \
--cpu host-passthrough \
--osinfo debian12 \
--disk path=$(GEN_DISK),format=qcow2,bus=virtio \
--disk path=$(GEN_SEED),device=cdrom,bus=sata \
--network network=lan-thurward,model=virtio \
--network network=wan-thurward,model=virtio \
--graphics none --console pty,target_type=serial \
Expand All @@ -229,6 +253,117 @@ gen-down:
echo " $(GEN_NAME): down"; \
fi

# ---------------------------------------------------------------------
# WAN-side sink VM (for TRex B-NN: avoids same-VM loopback shortcut)
# ---------------------------------------------------------------------

SINK_DISK := $(IMAGES_INST_DIR)/thur-sink.qcow2
SINK_NAME := thur-sink
SINK_WAN_IP := 203.0.113.200

.PHONY: sink-up
sink-up: net-up $(BASE_IMAGE).customised $(HARNESS_KEY)
@mkdir -p $(IMAGES_INST_DIR)
@if [ ! -f $(SINK_DISK) ]; then \
echo "Cloning base image to sink instance"; \
qemu-img create -f qcow2 -F qcow2 -b $(BASE_IMAGE) $(SINK_DISK) 20G; \
$(SCRIPTS_DIR)/render-cloud-init.sh sink gen $(SINK_DISK) $(HARNESS_KEY_PUB); \
fi
@if ! virsh -c $(LIBVIRT_URI) dominfo $(SINK_NAME) >/dev/null 2>&1; then \
virt-install --connect $(LIBVIRT_URI) \
--name $(SINK_NAME) \
--memory 1024 --vcpus 1 \
--cpu host-passthrough \
--osinfo debian12 \
--disk path=$(SINK_DISK),format=qcow2,bus=virtio \
--network network=wan-thurward,model=virtio \
--graphics none --console pty,target_type=serial \
--import --noautoconsole --noreboot; \
fi
@if [ "$$(virsh -c $(LIBVIRT_URI) domstate $(SINK_NAME))" != "running" ]; then \
virsh -c $(LIBVIRT_URI) start $(SINK_NAME); \
fi
@echo " $(SINK_NAME): running (WAN $(SINK_WAN_IP))"

.PHONY: sink-down
sink-down:
@if virsh -c $(LIBVIRT_URI) dominfo $(SINK_NAME) >/dev/null 2>&1; then \
virsh -c $(LIBVIRT_URI) destroy $(SINK_NAME) 2>/dev/null || true; \
virsh -c $(LIBVIRT_URI) undefine $(SINK_NAME) --nvram 2>/dev/null || true; \
echo " $(SINK_NAME): down"; \
fi

# ---------------------------------------------------------------------
# TRex generator lifecycle (replaces gen-up for RFC-9411 measurements)
# ---------------------------------------------------------------------

TREX_TARBALL := $(BENCH_DIR)/images/trex/trex-latest.tar.gz
TREX_DISK := $(IMAGES_INST_DIR)/thur-trex.qcow2
TREX_NAME := thur-trex
TREX_LAN_IP := 10.10.0.200

# Fetch TRex tarball (~270 MB). Cisco's CDN doesn't send the
# intermediate cert in the TLS chain, so we use -k and verify the
# tarball is a valid gzip afterwards.
$(TREX_TARBALL):
@mkdir -p $(dir $@)
@if [ ! -f $@ ]; then \
echo "Downloading TRex tarball (~270 MB)..."; \
curl -fSL -k --connect-timeout 30 -o $@.tmp \
https://trex-tgn.cisco.com/trex/release/latest; \
file $@.tmp | grep -q 'gzip compressed' || { echo "ERROR: not a gzip"; rm $@.tmp; exit 1; }; \
mv $@.tmp $@; \
fi
@ls -lh $@

.PHONY: trex-fetch
trex-fetch: $(TREX_TARBALL)

.PHONY: trex-up
trex-up: net-up $(BASE_IMAGE).customised $(HARNESS_KEY) $(TREX_TARBALL)
@mkdir -p $(IMAGES_INST_DIR)
@if [ ! -f $(TREX_DISK) ]; then \
echo "Cloning base image to TRex instance"; \
qemu-img create -f qcow2 -F qcow2 -b $(BASE_IMAGE) $(TREX_DISK) 30G; \
$(SCRIPTS_DIR)/render-cloud-init.sh trex gen $(TREX_DISK) $(HARNESS_KEY_PUB); \
fi
@if ! virsh -c $(LIBVIRT_URI) dominfo $(TREX_NAME) >/dev/null 2>&1; then \
echo "Defining $(TREX_NAME)"; \
virt-install --connect $(LIBVIRT_URI) \
--name $(TREX_NAME) \
--memory 4096 --vcpus 4 \
--cpu host-passthrough \
--osinfo debian12 \
--disk path=$(TREX_DISK),format=qcow2,bus=virtio \
--network network=lan-thurward,model=virtio \
--network network=wan-thurward,model=virtio \
--memorybacking hugepages=on \
--graphics none --console pty,target_type=serial \
--import --noautoconsole --noreboot; \
fi
@if [ "$$(virsh -c $(LIBVIRT_URI) domstate $(TREX_NAME))" != "running" ]; then \
virsh -c $(LIBVIRT_URI) start $(TREX_NAME); \
fi
@echo " $(TREX_NAME): running (LAN $(TREX_LAN_IP))"

.PHONY: trex-down
trex-down:
@if virsh -c $(LIBVIRT_URI) dominfo $(TREX_NAME) >/dev/null 2>&1; then \
virsh -c $(LIBVIRT_URI) destroy $(TREX_NAME) 2>/dev/null || true; \
virsh -c $(LIBVIRT_URI) undefine $(TREX_NAME) --nvram 2>/dev/null || true; \
echo " $(TREX_NAME): down"; \
fi

# B-00: reference path, empty ruleset. SUT is booted in pass-through
# (no rules.nft loaded — see candidates/nftables/rules-empty.nft).
B00_RESULT_DIR := $(RESULTS_DIR)/b-00/$(TODAY)

.PHONY: b-00
b-00: trex-up sink-up sub-up
@mkdir -p $(B00_RESULT_DIR)
@$(SCRIPTS_DIR)/b00.sh $(CAND) $(B00_RESULT_DIR) $(HARNESS_KEY) $(TREX_LAN_IP) $(SINK_WAN_IP)
@echo " result: $(B00_RESULT_DIR)/result.json"

# ---------------------------------------------------------------------
# Smoke test (Tier-0 reachability + iperf3 sanity)
# ---------------------------------------------------------------------
Expand All @@ -252,7 +387,7 @@ status:
@virsh -c $(LIBVIRT_URI) list --all

.PHONY: tear-down
tear-down: sub-down gen-down net-down
tear-down: sub-down gen-down trex-down sink-down net-down
@echo " harness fully torn down"

.PHONY: distclean
Expand Down
57 changes: 57 additions & 0 deletions bench/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,63 @@ make tear-down # destroy everything

Results land under `bench/results/smoke/<UTC-date>/` as JSON.

## Scope decision: throughput is Tier-1 only

The bench tree contains a TRex generator + sink-VM scaffolding
(`make trex-up`, `make sink-up`, `make b-00`) but **headline throughput
numbers (B-00 through B-13) are deliberately out of scope on Tier 0**.

Reasoning:

- The host is a developer workstation. The SUT VM has 1 vCPU; the
virtio-net path between qemu processes caps well below TRex's
software-mode ceiling, which itself caps well below TRex's normal
DPDK-NIC ceiling. **Any throughput number produced here measures the
virtio path, not the SUT's envelope.**
- `tests/benchmarks.md` § 0.6 already says B-00 is a per-environment
control: if the control itself is loss-limited, downstream B-NN
numbers are not reportable. On this Tier-0 setup the control is
always loss-limited.
- The right time to run B-00..B-13 is on the Tier-1 lab box
([`ROADMAP.md`](./ROADMAP.md)) — physical NIC + SR-IOV + RT kernel +
dedicated NUMA node. Until that hardware exists, those benchmarks
are intentionally skipped, not faked.

What stays in scope on Tier 0:

- **Smoke** (`make smoke`) — reachability + ruleset-loaded precondition
(per `tests/benchmarks.md` § 0.8).
- **Functional correctness** — rule translation discipline (§ 0.7),
scenarios.md, security-effectiveness checks.
- **Footprint metrics** (B-21 image size, B-22 RSS, B-23 launch time,
B-24 idle latency) — these are workstation-friendly and produce
per-candidate numbers that don't depend on throughput.

The TRex/sink scaffolding stays in the tree so the Tier-1 box can run
B-00..B-13 immediately when it arrives — no harness rewrite needed.

## Latest Tier-0 smoke run

Run on 2026-05-25 against the `nftables` candidate on an Arch
workstation (Intel i7-8700K @ 3.7 GHz, virtio-net, 1 vCPU SUT).
Verdict: ✅ `OK` — precondition (`§ 0.8`) satisfied.

| Probe | Result | Interpretation |
| ------------------------------------------- | ----------------- | ------------------------------------------------------------- |
| SUT `nftables-thurward.service` | `active` | Ruleset loaded; security-effectiveness precondition met |
| ICMP gen-LAN → SUT-LAN (`10.10.0.1`) | reachable | `input` chain permits ICMP on `enp1s0` as designed |
| ICMP gen-LAN → WAN (`203.0.113.50`) | dropped (correct) | `forward` chain default-deny working |
| TCP/443 gen-LAN → WAN | crossed firewall | `allow-github-https` rule active |
| iperf3 gen-LAN → gen-WAN-netns through SUT | **~14 Gbps** | TCP/5201 allowed; virtio-bounded (see Tier-0 caveat below) |

**Tier-0 caveat.** ~14 Gbps is the virtio-net path between two qemu
processes on the same host — not the SUT envelope. See the "Scope
decision" section above and [`ROADMAP.md`](./ROADMAP.md) Tier-1 for
where comparable-to-vendor numbers will come from.

Run-specific result JSON lands under `results/smoke/<UTC-date>/` (not
in git — the per-run output is gitignored).

## Methodology pointer

Every harness run **must** satisfy the methodology in
Expand Down
16 changes: 12 additions & 4 deletions bench/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,27 @@ characterising behaviour in thurward's ~1 Gbps v1 envelope.
- Single host (developer workstation)
- KVM + libvirt + virtio-net
- Host-internal libvirt bridges, no physical NICs
- iperf3 + ping for smoke; TRex (in a VM) for headline benchmarks
- iperf3 + ping for smoke; TRex scaffolding present but not used for
headline numbers at this tier (see below)

**What it can measure**

- Functional correctness (security-effectiveness precondition,
`tests/benchmarks.md` § 0.8 — does the firewall actually enforce its
rules?)
- Throughput up to ~1–2 Gbps with caveats
- Latency on the order of 100s of µs (host scheduler noise included)
- Footprint metrics (B-21..B-24): image size, RSS, launch time, idle
latency baseline
- Conntrack / NAT correctness at moderate scale

**What it can't measure**
**What it can't measure (and won't try to)**

- **Throughput (B-00..B-13).** Defer to Tier 1. The 1-vCPU SUT plus
virtio path bottlenecks well below TRex's software-mode ceiling,
which itself sits well below TRex's normal DPDK-NIC ceiling. Any
number here would be a harness measurement, not a SUT one. The
TRex/sink VMs are in the tree and runnable (`make b-00`), but the
resulting numbers are explicitly labelled "harness ceiling" in the
output JSON and not promoted as SUT capability.
- VPP's 10+ Mpps DPDK ceiling
- Sub-microsecond latency
- Sustained PPS at small frames against physical-NIC line rate
Expand Down
29 changes: 27 additions & 2 deletions bench/SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ sudo pacman -S --needed \
qemu-full libvirt virt-install \
dnsmasq bridge-utils iproute2 \
cloud-image-utils edk2-ovmf swtpm \
jq libguestfs iperf3
jq libguestfs guestfs-tools iperf3 \
tcpdump
```

`qemu-full` pulls in everything; on slim setups `qemu-base` +
Expand All @@ -38,6 +39,28 @@ sudo systemctl enable --now libvirtd.socket
sudo systemctl enable --now virtlogd.socket
```

## 3a. Run qemu as your user

`qemu:///system` defaults to running the qemu process as the
`libvirt-qemu` system user — which can't traverse `/home/<you>` to read
the bench instance disks stored under `bench/images/instances/`. Tell
libvirt to run qemu as your user instead:

```
sudo tee -a /etc/libvirt/qemu.conf >/dev/null <<EOF

# thurward-bench: run qemu as the invoking user so instance qcow2s
# stored under /home/<user>/Repositories/.../bench/images/instances/
# are readable by the qemu process.
user = "$USER"
group = "kvm"
EOF
sudo systemctl restart libvirtd.service
```

Without this, `make sub-up` fails with
`error: Cannot access storage file ... (as uid:955, gid:955): Permission denied`.

## 4. User groups

Your shell user needs to be in `libvirt` and `kvm`:
Expand Down Expand Up @@ -118,4 +141,6 @@ If both bridges came up, you're ready.
| `Could not access KVM kernel module: Permission denied` | User not in `kvm` group, or `/dev/kvm` permissions are wrong (`ls -l /dev/kvm` should be `0660 kvm`). |
| Wayland + libvirt graphical viewer doesn't open | Use `virsh console <vm>` (text) instead of `virt-viewer`. The harness doesn't need a GUI. |
| `network 'lan-thurward' is not active` after host reboot | Networks are not auto-started; run `make net-up` again. |
| nftables guest can't reach package mirrors | Cloud-init runs *after* the firewall ruleset loads; the guest is intentionally isolated. Install packages into the cloud-image with `virt-customize` instead. |
| nftables guest can't reach package mirrors | Cloud-init runs *after* the firewall ruleset loads; the guest is intentionally isolated. The Makefile pre-bakes all packages via `make base-customise` — re-run that target if you change `BASE_PACKAGES`. |
| `error: Cannot access storage file ... (as uid:955, gid:955)` | Run § 3a — `qemu:///system` is still running qemu as `libvirt-qemu`. Bench disks live under `/home`, which that user can't traverse. |
| Guest boots but `localhost login:` instead of `thur-*` | Cloud-init never read the seed. The harness injects the seed into `/var/lib/cloud/seed/nocloud/` on the instance qcow2 via `virt-customize`; if this step fails (e.g. `guestfs-tools` missing) cloud-init falls back to default state. Check `make sub-up` output. |
Loading
Loading