Iperf(at)Work

Iperf(at)Work / Technical description

Iperf(at)Work

Architecture, components, security, packaging and operation of Iperf(at)Work

Version 2.1.0Revised 2026-10-04Client and server

Iperf(at)Work is a browser interface for network performance tests with iperf3. This document describes the architecture, the components, the API, the security mechanisms, the packaging and installation model and operation. It is written for operations, security review and further development.

1. Purpose and scope

Product

Iperf(at)Work 2.1.0

Components

Caddy 2.11.4 (web server and reverse proxy), iperf-api (Python, standard library), iperf3 3.21 (static), web UI (HTML/CSS/JS), iperf-by-web-ctl, installation scripts

Use

Measuring bandwidth, retransmissions, jitter and packet loss between two machines; troubleshooting and acceptance tests of networks, VPN, WAN and virtual switches

Not intended for

Continuous monitoring, measurement from many agents, long-term storage of measurement series

Target platforms

Rocky Linux 9 / RHEL 9 / AlmaLinux 9 and VMware Photon OS 4/5, on bare metal, VMware VM/appliance and Proxmox VE VM (x86_64)

Run model

Ordinary Linux processes managed by systemd, installed from three RPM packages without repositories or internet

Languages

English and Norwegian (bokmål) in the web UI, API messages, manuals, license pages and scripts

2. Architecture

2.1. Overview

 Browser
   │ HTTP :80 (optional HTTP Basic password)
   ▼
 ┌────────────────────────── caddy.service (user caddy) ────────────────────────────┐
 │  /api/*  ──► reverse_proxy 127.0.0.1:9000 (flush_interval -1 for SSE)              │
 │  /*      ──► file_server /usr/share/iperf-by-web/html                               │
 │              index.html, ui.css, i18n.js, app.js, manual(.no).html, licenses(.no)  │
 │  security headers (CSP, X-Frame-Options, nosniff, Referrer-Policy)                 │
 └────────────────────────────────────────────────────────────────────────────────────┘
   │
   ▼ 127.0.0.1:9000
 ┌────────────────────────── iperf-api.service (user iperf-by-web) ──────────────────┐
 │  ThreadingHTTPServer ─► /api/start ─► subprocess iperf3 -c … (new process group)   │
 │                        watcher thread ─► tests/test-<id>.log + .meta.json          │
 │                       /api/output  ─► Server-Sent Events (follows the log file)     │
 │                       /api/server/status ─► systemctl is-active, ss, server log    │
 └────────────────────────────────────────────────────────────────────────────────────┘

 iperf3-server@5201.service, iperf3-server@5202.service (user iperf-by-web, server/both)
   iperf3 --server --port N --forceflush --timestamps --logfile /var/lib/iperf-by-web/server-N.log

2.2. Components

Component Language Responsibility

Caddy 2.11.4 (/usr/bin/caddy)

Go

Listens on port 80, serves the static pages, proxies /api/* to the API without buffering (live stream), adds security headers, optional HTTP Basic authentication (/etc/caddy/iperf-by-web.d/auth.caddy), access log with rotation

iperf-api (/usr/libexec/iperf-by-web/iperf-api)

Python 3

JSON API: validates test parameters, starts and stops iperf3 without a shell, stores log and metadata per test, parses the summary, streams the output with Server-Sent Events, reads the server status

iperf3 3.21 (/usr/bin/iperf3)

C

The measurement itself, as client (started by the API) and as server (iperf3-server@<port>)

Web UI (index.html, ui.css, i18n.js, app.js)

HTML/CSS/JS

Form, live graph (canvas), result tiles, summary, history with CSV export, server status, EN/NO

iperf-by-web-ctl

bash

Applies the configuration (services, firewall), status, role change, language, password, self test

install.sh, check-prereqs.sh, uninstall.sh, lib/common.sh

bash

Offline installation, requirement check, uninstallation – in English and Norwegian

2.3. Process and thread model

The API uses ThreadingHTTPServer with one thread per request. Each test is started as a separate process in a new process group (start_new_session=True), with stdout and stderr to the log file. A watcher thread waits for the process, writes the final state and the parsed result to the metadata file and releases the "running test". Only one client test can run at a time (lock + process check). SSE requests hold one thread each while the test runs and send a keep-alive comment about every 10 seconds.

On stop, SIGINT is sent to the process group so that iperf3 prints its summary; after 3 seconds SIGKILL follows. When the API starts, tests still marked running (interrupted by a restart) are marked failed with a note.

3. Measurement

3.1. The command

The API builds an argument list (never a shell string):

iperf3 -c <server> -p <port> -t <duration> -i <interval> -P <streams> --forceflush -f m
       [-O <omit>] [-u -b <bandwidth|1G>] [-b <bandwidth>] [-w <window>] [-l <length>]
       [-R | --bidir] [-B <source address>] [-4 | -6]

--forceflush makes every interval line appear at once (live view), and -f m gives every rate in Mbit/s.

3.2. Validation

Field Rule iperf3

server

^[A-Za-z0-9]([A-Za-z0-9.\-:]{0,251}[A-Za-z0-9])?$ (host name, IPv4, IPv6)

-c

port

1–65535

-p

duration

1–MAX_DURATION (3600)

-t

parallel

1–128

-P

interval

1–60

-i

omit

0–60 and less than the duration

-O

protocol

tcp / udp

-u

direction

normal / reverse / bidir

-R / --bidir

bandwidth

^[0-9]{1,6}(\.[0-9]{1,3})?[KkMmGg]?$

-b

window, length

^[0-9]{1,7}[KkMm]?$

-w, -l

bind

^[0-9A-Fa-f.:]{2,45}$ (local address)

-B

ipversion

auto / 4 / 6

-4 / -6

3.3. Parsing the result

The summary lines (ending in sender or receiver) are parsed with a regular expression that handles [ 5], [SUM], [ 5][TX-C] and [SUM][RX-C]. When [SUM] lines exist (several streams), only they are used. For each line the direction (up = TX-C, down = RX-C, otherwise none), the measuring end, the rate converted to Mbit/s, the amount transferred and either retransmissions (TCP, sender) or jitter, lost/total datagrams and loss in percent (UDP) are stored. The message iperf3: error - … is stored for failed tests.

The web UI parses the interval lines in the same way as they arrive, for the graph and the tiles; lines marked (omitted) are skipped.

3.4. Storage

File

Content

/var/lib/iperf-by-web/tests/test-<id>.log

Command and complete iperf3 output

/var/lib/iperf-by-web/tests/test-<id>.meta.json

Parameters, state (running, done, failed, stopped), times, exit code, summary lines, parsed result, error message

/var/lib/iperf-by-web/server-<port>.log

Server log with time stamps (logrotate: weekly, 8 weeks, compressed)

The ID is the start time YYYYMMDD-HHMMSS (with -n on collision). Metadata is written atomically (temporary file + os.replace). When the number passes KEEP_TESTS, the oldest are deleted.

4. Web interface

4.1. Structure

The page follows the style of SYSLOG-Tools(at)Work and SNMP-Trap-Tools(at)Work: dark green top bar with icon, name, status badge and host name; ? User Manual and EN | NO on the right; tabs; Bootstrap-like form controls; footer with Licenses · Third-party notices · User Manual. Everything is written for the project – no external scripts, style sheets, fonts or images – so the pages work without internet.

Tab

Content

Run test

Network setup, test parameters, advanced options, start/stop, command line, four tiles (last interval, average, maximum, progress), graph (two lines with --bidir), summary table, output

History

Filterable table of the newest 50 tests, expandable rows with command and summary, log download, CSV export (UTF-8 with BOM)

Server

State per port (systemd + listening socket), number of connections and peers (ss), uptime, server log per port; refreshed every 3 seconds

The tabs shown depend on the role (client: Run test and History, server: Server, both: all). The chosen tab is remembered in the address (#tab=). If the browser is closed during a test, the page reconnects to the running test on the next visit.

4.2. Language (i18n)

English is the source language. i18n.js holds the English → Norwegian dictionary and an engine that at start-up records every text node, placeholder, title and aria-label found in the dictionary, and switches them when the user presses EN or NO. Texts built by script use t('English text', {var}). The choice is stored in localStorage and a cookie (iperf_by_web_lang). Without a stored choice, UI_LANG from the configuration is used (through /api/status).

The API chooses the language of error messages from the header X-UI-Lang (sent by the page), otherwise UI_LANG. The manuals and license pages are separate files per language (manual.html / manual.no.html, licenses.html / licenses.no.html); the ? User Manual button opens the right language at the chapter of the tab (#s3, #s4, #s5). iperf3 output is not translated.

4.3. HTTP API

Method

Path

Description

GET

/api/status

Mode, host name, IP, versions (API, iperf3, Caddy), running test, defaults, ui_lang, uptime

POST

/api/start

Starts a test (JSON, see validation). 200 started, 400 invalid, 409 busy or server mode

POST

/api/stop

Stops the running test (SIGINT, then SIGKILL)

GET

/api/output?id=

SSE: data: per line, finally event: done with the metadata

GET

/api/tests

The last 50 tests (without PID)

GET

/api/log?id=

The log as a text/plain attachment

GET

/api/server/status

Ports, systemd state, listening, connections, peers, log excerpt (80 lines)

5. Security

5.1. Threat model

Iperf(at)Work is an administration tool for a closed network. The main risk is that someone without permission uses the web UI to send large amounts of traffic (fill links) towards arbitrary targets. The measures below limit this; the web UI should still only be reachable from a trusted management network.

5.2. Measures in the web layer

Measure

Description

Access

Optional HTTP Basic password (bcrypt, cost 14) for the whole site including the API (iperf-by-web-ctl password set). Optional restriction of the web port to one network in firewalld (ADMIN_NET).

CSRF

Every POST request must carry X-Requested-With: iperf-by-web-ui. Other web sites cannot set such a header without a CORS preflight, which is never approved. The request body is always read before rejecting, so that keep-alive connections from Caddy stay in sync.

Injection

iperf3 is started with an argument list (no shell); every field is validated with allow-lists (see 3.2). Test IDs are validated (^[0-9]{8}-[0-9]{6}(-[0-9]{1,3})?$) before they are used in file paths.

XSS

All text from the server is inserted with textContent or escaped (esc()) before innerHTML.

Headers

Content-Security-Policy (own resources only, frame-ancestors 'none'), X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer; the Server header is removed.

Resources

One test at a time, duration at most MAX_DURATION, at most 128 streams, request body at most 64 KiB, history limited to KEEP_TESTS.

API binding

The API listens only on 127.0.0.1:9000 and is reached only through Caddy. Caddy’s admin API listens only on localhost:2019.

5.3. Process isolation (systemd)

Service

Hardening

caddy

User caddy, only CAP_NET_BIND_SERVICE (also as bounding set), NoNewPrivileges, ProtectSystem=full, PrivateTmp

iperf-api

User iperf-by-web, NoNewPrivileges, ProtectSystem=strict (writes only /var/lib/iperf-by-web), ProtectHome, PrivateTmp, PrivateDevices, ProtectKernelTunables/Modules, ProtectControlGroups, RestrictSUIDSGID, LockPersonality, RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_NETLINK, UMask=0027

iperf3-server@

User iperf-by-web, NoNewPrivileges, ProtectSystem=strict, ProtectHome, PrivateTmp, PrivateDevices, ProtectKernelModules, ProtectControlGroups, RestrictSUIDSGID

SELinux: the services run as unconfined_service_t; the installation sets the labels with restorecon.

5.4. Known limitations

  • The web UI uses plain HTTP. The password (if used) is sent base64 encoded. Use a trusted network and ADMIN_NET. HTTPS can be set up in the Caddyfile with your own certificates (tls /path/cert.pem /path/key.pem).

  • iperf3 is built without OpenSSL, so iperf3’s own authentication (--username, --rsa-public-key-path) is not available. The iperf3 server accepts tests from everyone who can reach the ports; restrict with the firewall.

  • One iperf3 server can only run one test at a time per port.

  • With -B over loopback aliases, UDP tests fail in iperf3 (affects only test environments, not real network cards).

  • No user roles: everyone who gets in can start and stop tests.

6. Packaging and installation

6.1. The RPM packages

Package

Architecture

Content and dependencies

iperf3-3.21-2.static.el9

x86_64

/usr/bin/iperf3 (static, musl), man page, licenses (iperf3 + musl). No dependencies.

caddy-2.11.4-2.el9

x86_64

/usr/bin/caddy (official binary, SHA-512 verified), caddy.service, license + the licenses of 144 Go modules. Requires systemd, (shadow-utils or shadow).

iperf-by-web-2.1.0-1.el9

noarch

API, web UI, manuals, license pages, Caddyfile, configuration, systemd units, iperf-by-web-ctl, uninstall.sh, documentation. Requires caddy >= 2.7, iperf3 >= 3.9, python3, (iproute or iproute2), util-linux, systemd.

The dependencies are written so that they resolve on both Rocky/RHEL 9 and Photon OS (boolean dependencies, rpm ≥ 4.13). Configuration files are %config(noreplace).

6.2. Installed layout

/etc/caddy/Caddyfile                       %config(noreplace)
/etc/caddy/iperf-by-web.d/                  0750 root:caddy  (auth.caddy when a password is set)
/etc/iperf-by-web/iperf-by-web.conf          %config(noreplace)
/etc/logrotate.d/iperf-by-web
/usr/bin/caddy, /usr/bin/iperf3
/usr/libexec/iperf-by-web/iperf-api
/usr/sbin/iperf-by-web-ctl
/usr/share/iperf-by-web/html/               web UI, manual(.no).html, licenses(.no).html
/usr/share/iperf-by-web/uninstall.sh, lib/common.sh
/usr/share/doc/iperf-by-web/                documents (no + en/ + img/)
/usr/share/licenses/{iperf-by-web,caddy,iperf3}/
/usr/lib/systemd/system/{caddy,iperf-api,iperf3-server@}.service
/var/lib/iperf-by-web/                      0750 iperf-by-web (tests/, server-<port>.log)
/var/lib/caddy, /var/log/caddy             0750 caddy

6.3. The installer

install.sh runs seven steps: requirements (architecture, platform, systemd, rpm, Python ≥ 3.7, ss, useradd, runuser, checksums, free ports), packages (dnf --disablerepo='*' install or rpm -Uvh; packages at the right version are skipped), configuration (new keys are added to an older file), password, SELinux, iperf-by-web-ctl apply and verification (services, self test, reachability of the default server). All messages exist in English and Norwegian (--lang).

iperf-by-web-ctl apply validates the Caddyfile as the user caddy, stops every iperf3-server@ instance and starts only those the role needs, restarts/reloads the API and Caddy and updates firewalld (removes old iperf ports 5200–5299, opens the web port – as a rich rule for ADMIN_NET if set – and the server ports for tcp and udp).

7. Configuration

/etc/iperf-by-web/iperf-by-web.conf is read by the API (systemd EnvironmentFile) and by iperf-by-web-ctl.

Key

Default

Meaning

IPERF_MODE

client

client, server, both

IPERF_SERVER_PORTS

"5201 5202"

Ports for iperf3-server@

DEFAULT_SERVER, DEFAULT_PORT

172.16.101.61, 5201

Pre-filled in the UI

UI_LANG

en

Default language of the UI, API messages and iperf-by-web-ctl

HOST_IP

empty

Shown IP address (empty: the address used towards DEFAULT_SERVER, found with a UDP socket without sending packets)

MAX_DURATION

3600

Longest test

KEEP_TESTS

200

Tests in the history

API_BIND, API_PORT

127.0.0.1, 9000

API address (also in the Caddyfile)

WEB_PORT

80

Web port (firewall; also in the Caddyfile)

MANAGE_FIREWALL

yes

firewalld managed by iperf-by-web-ctl

ADMIN_NET

empty

The web port is opened only for this network

OUTPUT_DIR, IPERF_BIN

/var/lib/iperf-by-web, /usr/bin/iperf3

Paths

8. Operations

Task

Command

Status

iperf-by-web-ctl status

Logs

iperf-by-web-ctl logs, journalctl -u iperf-api -f, /var/log/caddy/iperf-by-web-access.log

Role change

iperf-by-web-ctl mode client|server|both

Password

iperf-by-web-ctl password set [user] / off / status

Self test

iperf-by-web-ctl selftest (local iperf3 test on 127.0.0.1:5299, API and web UI)

Restart

systemctl restart iperf-api caddy

Uninstall

/usr/share/iperf-by-web/uninstall.sh [--all] [--purge]

Disk use: one test typically takes 2–20 KB (log + metadata); 200 tests stay below 5 MB. The server logs are rotated weekly.

9. Build and quality assurance

9.1. Build

Tool

Does

tools/build-iperf3-static.sh

Builds iperf3 from sources/iperf-3.21.tar.gz with musl-gcc (static, without OpenSSL/SCTP). The result is byte-identical to the binary in 2.0.0.

tools/fetch-go-licenses.py

Reads the module list in caddy (go version -m) and fetches the license and NOTICE files of each module (needs internet)

tools/build-licenses.py

Builds THIRD_PARTY_NOTICES.txt, third_party_inventory.json, licenses/caddy/THIRD-PARTY-GO-MODULES.txt and the license pages (offline)

tools/build-manuals.py

Builds manual.html and manual.no.html from Markdown with pandoc (images embedded)

tools/make-screenshots.py

Starts the whole stack locally, runs real tests and takes screenshots in both languages (Playwright)

tools/build-rpms.sh

Builds the three RPMs (reproducible time stamps)

tools/build-package.sh

All of the above + self test + SHA256SUMS + Iperf-by-web-2.1.0-offline.tar.gz (+ .sha256)

9.2. Self test

python3 tools/selftest.py runs static checks (Python and bash syntax, ShellCheck, version numbers, translation coverage, no external resources, documents), unit tests of the summary parsing, integration tests against a real API and a real iperf3 server (TCP, UDP, --bidir, SSE, stop, failed test, validation, CSRF, keep-alive, language, history) and package tests (names, versions, dependencies, files). See Revisjonsrapport.md (Norwegian review report) for the results.

Screenshots, installation and how client and server are used together: Back to the front page