Multicast(at)Work

Multicast(at)Work / Technical description

Multicast(at)Work

Architecture, test tools, security, installation and operation of Multicast(at)Work – a web GUI for iperf 2 and ping that tests unicast, multicast and MTU in closed networks

Version 3.0.0Revised 2026-10-04Receiver, Sender and MTU Probe

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 (app.py, i18n.py, templates, static/), iperf 2.2.1 (static build), install scripts (install.sh, uninstall.sh, check-prereqs.sh), self test (selftest.py)

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

Components and data flow
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

app.py

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

i18n.py

Norwegian translations (English is the key) and the texts sent to JavaScript

templates/

base.html (blue top bar with status, EN | NO and Log out, footer), login.html, index.html (five tabs), manual.html, licenses.html

static/

Bootstrap 5.3.8 (CSS only), app.css (same look as SNMP Trap Sender), app.js (tabs, quick settings, sessions, log, history, interfaces), icon, screenshots for the manual

manual/

The user manual as HTML, built from docs/*.md by tools/build-manuals.py

vendor/iperf/

iperf 2.2.1 for x86_64 and aarch64, license texts and BUILD-INFO.txt

lib/common.sh

Shared shell functions: language, output, platform detection, choice of iperf for the CPU

tools/selftest.py

Self test against an installed server (standard library only)

2.3. Process model and privileges

Process User Privileges

waitress (multicast-tester.service)

mct

AmbientCapabilities=CAP_NET_RAW CAP_NET_ADMIN, the same CapabilityBoundingSet, NoNewPrivileges. 16 threads.

iperf, ping

mct (inherited)

Inherit the two capabilities. CAP_NET_RAW is needed for SO_BINDTODEVICE (interface selection) and ICMP, CAP_NET_ADMIN for joining a group on a specific interface.

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

iperf -s -p P -i 1 and iperf -s -u -B G[%IF] -p P -i 1

TCP only

iperf -s -p P -i 1

UDP / Multicast

iperf -s -u -B G[%IF] -p P -i 1

-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.

  1. ping -n -c 5 [-I IF] U

  2. iperf -c U -p P -t T -i 1

  3. 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

  1. A normal ping (-c 2 -W 2). If the target does not answer, the probe stops with an explanation.

  2. 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.

  3. 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.

  4. 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_LINES lines; 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_TTL seconds. With MCT_MAX_SESSIONS sessions, 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/sessions and reconnects to the newest running session of each type. After that /api/sessions is 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

/login

GET, POST

Sign-in page. Form: user, password, next

/logout

POST

Signs out. Form: csrf (token in the session)

/

GET

Front page with the tabs Receiver, Sender, MTU Probe, History and Interfaces

/manual

GET

The user manual in the chosen language

/licenses

GET

LICENSE.txt (#license) and THIRD_PARTY_NOTICES.txt (#third-party)

/licenses/third_party_inventory.json

GET

Machine-readable list of third-party components

/lang/<en|no>?next=<path>

GET

Stores the language in the cookie mct_lang and returns to next (local paths only)

/api/start_receiver

POST

Starts a Receiver. JSON: mode, multicast_addr, port, iface

/api/start_sender

POST

Starts a Sender. JSON: mode, unicast_addr, multicast_addr, port, duration, ttl, bandwidth, iface

/api/start_mtu_probe

POST

Starts an MTU probe. JSON: target, iface, sizes, df

/api/log/<id>?offset=N

GET

New log lines, running, total

/api/stop/<id>

POST

Stops the session and all its processes

/api/download/<id>

GET

The whole log as multicast_<type>_<time>.log

/api/sessions

GET

All sessions with id, type, label, running, stopped, number of lines, start and end time

/api/interfaces

GET

Interfaces with MTU, IPv4 addresses, default route and the flags up, multicast and loopback (from /sys/class/net/<name>/flags)

/healthz

GET

{"ok":true,"version":"3.0.0","iperf":"2.2.1"}. No login required.

4.2. Language (i18n)

  • The English text is the key, and i18n.NO holds the Norwegian translation. tests/test_i18n.py checks that every text in the templates, app.py and app.js is translated and that the placeholders match.

  • The language comes from the cookie mct_lang, otherwise from MCT_LANG. Templates use _(), app.py uses tr(), 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.

  • /licenses shows LICENSE.txt and THIRD_PARTY_NOTICES.txt from MCT_DOC_DIR (/opt/multicast-tester).

  • The user manual is built from docs/Brukermanual.md and docs/en/User-Manual.md with 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, /healthz and /static. Pages are redirected to /login, API calls get 401 with JSON (without WWW-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 for MCT_SESSION_HOURS, default 12 hours). The key is MCT_SECRET_KEY, which install.sh creates (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.sh creates 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-site is 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

ipaddress.IPv4Address (rejects leading zeros and octal notation). A unicast address cannot be multicast, broadcast or 0.0.0.0. The group must be in 224.0.0.0/4.

Host names (MTU target)

Regular expression after RFC 1123. Cannot start with -.

Port, duration, TTL

Integers: 1–65535, 1–300, 1–255

Bandwidth

^\d+(\.\d+)?[kKmMgG]?$

Interface

^[A-Za-z0-9][A-Za-z0-9._@:-]{0,14}$ and must exist in /sys/class/net

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-net or 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.

  • click 8.1.8 has a known vulnerability that cannot be reached from the application (see Revisjonsrapport.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

/opt/multicast-tester/app/

app.py, i18n.py, templates/, static/, manual/

/opt/multicast-tester/bin/iperf

iperf 2.2.1 for the machine’s CPU (or a copy of the system iperf with --system-iperf)

/opt/multicast-tester/lib/python/

The Python packages (unpacked wheels)

/opt/multicast-tester/lib/sh/common.sh

For uninstall.sh

/opt/multicast-tester/docs/, licenses/

The documents, and license texts and build information for iperf

/opt/multicast-tester/{LICENSE.txt,THIRD_PARTY_NOTICES.txt,third_party_inventory.json,VERSION,README.txt}

Also shown in the GUI

/opt/multicast-tester/{uninstall.sh,selftest.py}

Uninstall and self test

/etc/multicast-tester/multicast-tester.env

Settings and password (0640 root:mct)

/etc/systemd/system/multicast-tester.service

The service

/etc/systemd/system/multicast-tester.service.d/10-container.conf

Only in containers

/etc/sysctl.d/90-multicast-tester.conf

net.ipv4.conf.{all,default}.rp_filter = 2

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.sh from iperf 2.2.1 (iperf_2.2.1+dfsg.orig.tar.xz, SHA-256 checked) with zig 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 by getopt_long from musl, so the file contains no copyleft code (see Lisensrapport.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 (--port)

TCP

The web GUI. Can be restricted to --admin-net.

5001–5010 (--iperf-ports)

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

systemctl status multicast-tester

Service log

journalctl -u multicast-tester -f

Self test

sudo python3 /opt/multicast-tester/selftest.py [--lang no] [--quick]

Change password, port or language

sudo ./install.sh --password … --port … --lang … from the package folder

Restart

sudo systemctl restart multicast-tester

Uninstall

sudo /opt/multicast-tester/uninstall.sh [--keep-config]

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.

  • shellcheck on every shell script, pyflakes and bandit on the Python code, pip-audit on 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.

Screenshots, installation and test scenarios: Back to the front page