Multicast(at)Work is a web interface for iperf 2 and ping. It checks that a network carries unicast and multicast correctly, and finds the largest packet size (MTU) on a path through the network. It is installed offline on Rocky Linux 9, in VMware and in Proxmox VE, and runs as a single systemd service without Docker. This document describes the architecture, how the tests are run, the security mechanisms, the installation model and the interfaces. It is written for operations, security review and further development.
1. Purpose and scope
Product |
Multicast(at)Work 3.0.0 |
Components |
Web GUI ( |
Use |
Checking multicast within and between VLANs (IGMP join, snooping, querier, PIM and TTL), unicast throughput as a baseline, and the MTU of a path (jumbo frames, tunnels, PPPoE) |
Not intended for |
Continuous monitoring, load generation in production networks or IPv6 measurements |
Target platforms |
Rocky Linux 9 / RHEL 9 / AlmaLinux 9 / Oracle Linux 9 (primary) and VMware Photon OS, on a physical machine, as a VM in VMware or Proxmox VE, or as an LXC container in Proxmox VE. x86_64 and aarch64. |
Run model |
One Flask application under waitress, managed by systemd, that starts iperf and ping as separate processes. No containers, no pip, no EPEL, no internet access. |
Languages |
English and Norwegian (bokmål) in the web interface, log lines, error messages, user manual and install scripts |
License |
MIT |
2. Architecture
2.1. Overview
Browser (administrator) │ HTTP :5152, sign-in page (signed session cookie) or Basic Auth, │ X-Requested-With: mct on every POST ▼ waitress 3.0 + Flask 3.1 (app.py, i18n.py) user mct (not root) │ CAP_NET_RAW, CAP_NET_ADMIN │ sessions in memory: id → processes, log, state │ every command in its own process group, no shell ├─ /opt/multicast-tester/bin/iperf -s … receiver (TCP and/or UDP group) ├─ /opt/multicast-tester/bin/iperf -c … sender (TCP / UDP multicast) ├─ ping -M do|want -s N … ping and MTU probe └─ ip -4 -o addr addresses for the interface list ▼ Interface ── IGMP join/leave, UDP stream, TCP, ICMP ──▶ test network ◀── other installations / iperf 2
2.2. Code modules
| File | Contents |
|---|---|
|
The Flask application: routes, authentication, CSRF check, security headers, validation, session handling, running iperf and ping, the MTU probe, language choice, license and manual pages |
|
Norwegian translations (English is the key) and the texts sent to JavaScript |
|
|
|
Bootstrap 5.3.8 (CSS only), |
|
The user manual as HTML, built from |
|
iperf 2.2.1 for x86_64 and aarch64, license texts and |
|
Shared shell functions: language, output, platform detection, choice of iperf for the CPU |
|
Self test against an installed server (standard library only) |
2.3. Process model and privileges
| Process | User | Privileges |
|---|---|---|
|
|
|
|
|
Inherit the two capabilities. |
Every command is started with start_new_session=True, that is in its own process group. Stop sends SIGTERM to the group and SIGKILL after two seconds. When the service stops, KillMode=control-group does the same for all processes.
3. The test tools
3.1. Receiver
| Mode | Commands (in parallel) |
|---|---|
Both |
|
TCP only |
|
UDP / Multicast |
|
-B group%interface makes iperf join the group (IGMP join) on the chosen interface. Without an interface the interface of the default route is used. The Receiver runs until it is stopped. -i 1 gives one line per second with bandwidth, jitter and lost/received datagrams.
3.2. Sender
The steps run in sequence. A failing step does not stop the following ones.
-
ping -n -c 5 [-I IF] U -
iperf -c U -p P -t T -i 1 -
iperf -u -c G[%IF] -p P -t T -T TTL -b B -i 1
TTL (1–255) decides how many routers multicast can pass. The bandwidth -b is a number with an optional k, M or G.
3.3. MTU probe
-
A normal ping (
-c 2 -W 2). If the target does not answer, the probe stops with an explanation. -
For every size S (29–9216 bytes, at most 1000 sizes, sorted and de-duplicated):
ping -n -c 1 -W 2 -M do|want -s S-28 [-I IF] target. 28 bytes are the IP and ICMP headers. -
The output is interpreted: reply (✓ with rtt),
message too long, mtu=N(larger than the local interface),frag needed(too large for the path), no reply, timeout, unknown host. -
With DF on, the largest size that got a reply is the path MTU. With DF off the result says clearly that it is not the MTU. If smaller sizes fail than the largest one that worked, the result is flagged as inconsistent.
3.4. Sessions and logs
-
A session is a Receiver, a Sender or an MTU probe. It has a UUID, type, label, language, process list, log and state.
-
Every log line gets a time stamp. The log is limited to
MCT_MAX_LOG_LINESlines; the oldest are dropped, and the offset in the API accounts for that, so the browser never loses or duplicates lines. -
Finished sessions are removed after
MCT_SESSION_TTLseconds. WithMCT_MAX_SESSIONSsessions, the oldest finished ones are removed first. If all are running, the API answers 429. -
The browser asks for new lines every 0.8 seconds (
/api/log/<id>?offset=N). When the front page opens, it fetches/api/sessionsand reconnects to the newest running session of each type. After that/api/sessionsis fetched every 3 seconds for the History tab and the status badge in the top bar.
4. Web interface
4.1. Pages and HTTP API
| Path | Method | Contents |
|---|---|---|
|
GET, POST |
Sign-in page. Form: |
|
POST |
Signs out. Form: |
|
GET |
Front page with the tabs Receiver, Sender, MTU Probe, History and Interfaces |
|
GET |
The user manual in the chosen language |
|
GET |
|
|
GET |
Machine-readable list of third-party components |
|
GET |
Stores the language in the cookie |
|
POST |
Starts a Receiver. JSON: |
|
POST |
Starts a Sender. JSON: |
|
POST |
Starts an MTU probe. JSON: |
|
GET |
New log lines, |
|
POST |
Stops the session and all its processes |
|
GET |
The whole log as |
|
GET |
All sessions with id, type, label, |
|
GET |
Interfaces with MTU, IPv4 addresses, default route and the flags |
|
GET |
|
4.2. Language (i18n)
-
The English text is the key, and
i18n.NOholds the Norwegian translation.tests/test_i18n.pychecks that every text in the templates,app.pyandapp.jsis translated and that the placeholders match. -
The language comes from the cookie
mct_lang, otherwise fromMCT_LANG. Templates use_(),app.pyusestr(), and JavaScript gets its texts as JSON in<body data-i18n>. -
The log lines the program writes itself (headers, MTU results, "exited (code N)") are translated into the language the session was started in. Output from iperf and ping is shown unchanged.
-
API error messages use the language of the cookie.
4.3. Licenses and manual
-
The footer on every page: Multicast(at)Work 3.0.0 · Licenses · Third-party notices · User manual.
-
/licensesshowsLICENSE.txtandTHIRD_PARTY_NOTICES.txtfromMCT_DOC_DIR(/opt/multicast-tester). -
The user manual is built from
docs/Brukermanual.mdanddocs/en/User-Manual.mdwith pandoc when the package is built. The table of contents goes into the side bar.
5. Security
5.1. Access control
-
Signing in is required for everything except
/login,/lang,/licenses,/healthzand/static. Pages are redirected to/login, API calls get401with JSON (withoutWWW-Authenticate, so the browser does not show its own dialog). -
The sign-in page sets a signed session cookie
mct_session(Flask,HttpOnly,SameSite=Strict, valid forMCT_SESSION_HOURS, default 12 hours). The key isMCT_SECRET_KEY, whichinstall.shcreates (secrets.token_hex(32)) and keeps on reinstall. The session is cleared at sign-in (no fixation) and holds a tag derived from the password, so a new password signs out everybody. -
After 5 wrong passwords from one address it has to wait 60 seconds. Failed attempts are logged to the journal.
-
Scripts can use HTTP Basic Auth with the same user name and password. User name and password are compared with
hmac.compare_digest.install.shcreates a random password (secrets.token_urlsafe(12)) if none exists. -
Log out is a POST form with a CSRF token from the session.
-
CSRF: every POST must carry
X-Requested-With: mct. Another web site cannot send that header without a CORS request, which the application never allows.Sec-Fetch-Site: cross-siteis refused as well. -
Security headers on every response:
Content-Security-Policy: default-src 'self'; style-src 'self'; script-src 'self'; img-src 'self' data:; frame-ancestors 'none'; form-action 'self'; base-uri 'none',X-Frame-Options: DENY,X-Content-Type-Options: nosniff,Referrer-Policy: same-origin,Cache-Control: no-store. -
The language switch and the sign-in page accept only local paths in
next(no open redirect).
5.2. Validation
| Field | Check |
|---|---|
IP addresses |
|
Host names (MTU target) |
Regular expression after RFC 1123. Cannot start with |
Port, duration, TTL |
Integers: 1–65535, 1–300, 1–255 |
Bandwidth |
|
Interface |
|
MTU sizes |
List of at most 1000 numbers, clamped to 29–9216 |
JSON |
The body must be a JSON object, otherwise HTTP 400 |
Every command is an argument list (subprocess.Popen([…])), never a shell string.
5.3. Known limitations
-
The GUI uses HTTP, so the password and the session cookie travel unencrypted. Use a management network,
--admin-netor an SSH tunnel. -
A logged-in user can send traffic at any rate to any address. That is what the tool is for.
-
One shared login, no roles.
-
Sessions are kept in memory and are lost on restart.
-
click8.1.8 has a known vulnerability that cannot be reached from the application (seeRevisjonsrapport.md).
6. Installation and file layout
6.1. The installer
install.sh runs these steps: package check (SHA256SUMS) → platform (OS, architecture, systemd-detect-virt) → requirements (python3 ≥ 3.9, ping, ip; tries dnf/tdnf only if something is missing) → choice and check of iperf → user mct → files → Python packages → settings → rp_filter → service (with the container drop-in in LXC) → firewall → platform notes → self test → summary. The exit code is 0 (OK), 1 (error) or 2 (installed, but the self test found problems).
All scripts speak English and Norwegian (--lang, otherwise $LANG).
6.2. Installed layout
| Path | Contents |
|---|---|
|
|
|
iperf 2.2.1 for the machine’s CPU (or a copy of the system iperf with |
|
The Python packages (unpacked wheels) |
|
For |
|
The documents, and license texts and build information for iperf |
|
Also shown in the GUI |
|
Uninstall and self test |
|
Settings and password (0640 root:mct) |
|
The service |
|
Only in containers |
|
|
The application is replaced as a whole on upgrade (app.new → app), so files removed in a new version disappear.
6.3. Python packages
The wheels in vendor/wheels are unpacked directly with python3 -m zipfile into /opt/multicast-tester/lib/python, which the service finds through PYTHONPATH. pip and virtualenv are not used. MarkupSafe is chosen by Python version and CPU (cp39, cp311 and cp312, x86_64 and aarch64). Without a matching wheel another one is used, and MarkupSafe then falls back to its pure-Python implementation. Finally the installer checks that flask, waitress, jinja2 and markupsafe can be imported.
6.4. iperf
-
Built by
tools/build-iperf.shfrom iperf 2.2.1 (iperf_2.2.1+dfsg.orig.tar.xz, SHA-256 checked) withzig cc -target <arch>-linux-musl -static. The result is a static file without dependencies that runs on all common Linux distributions. -
gnu_getopt(LGPL) is replaced bygetopt_longfrom musl, so the file contains no copyleft code (seeLisensrapport.md). The rest of the source is unchanged. -
The build is reproducible: the same source and compiler give the same SHA-256, which is listed in
vendor/iperf/BUILD-INFO.txt.
6.5. Upgrading from 2.x
Version 2 lived directly in /opt/multicast-tester with a .venv and kept its settings in /etc/multicast-tester.env. install.sh detects this, takes port, user and password from the old file, moves it to .v2-migrated, replaces the example password change-me and removes the old files.
7. Network and external resources
7.1. Ports
| Port | Protocol | Use |
|---|---|---|
5152 ( |
TCP |
The web GUI. Can be restricted to |
5001–5010 ( |
TCP and UDP |
The iperf tests (GUI default: 5001) |
– |
IGMP (protocol 2) |
Group membership and answers to the querier |
– |
ICMP |
Ping and MTU probe (outgoing, with replies) |
rp_filter = 2 (loose mode) stops the kernel from dropping multicast that arrives on another interface than the route back to the sender.
7.2. External resources
None. Nothing is downloaded at installation or run time, and the web pages only load files from the server itself. Name resolution is only used if the MTU target is given as a host name.
8. Operation
| Task | Command |
|---|---|
Status |
|
Service log |
|
Self test |
|
Change password, port or language |
|
Restart |
|
Uninstall |
|
The service uses few resources when no test runs (about 40 MB of memory). During a test the bandwidth decides the CPU use of iperf.
9. Quality assurance
-
42 unit tests (pytest) for sign-in and log out, validation, security, language, sessions, Stop and MTU parsing. iperf and ping are replaced by small scripts, so the tests need no network.
-
shellcheckon every shell script,pyflakesandbanditon the Python code,pip-auditon the dependencies. -
Copyleft scan of every file in the package (
tools/gen-third-party.py --scan). -
The self test (19 checks, also signing in and out with the form) runs on every installation, with a real multicast stream from the Sender to the Receiver on the same host.
-
Browser tests in Chromium in both languages, in all five tabs, on the sign-in page and at phone width.
-
Details and the acceptance test for the target platforms are in
Revisjonsrapport.md.