Ping(at)Work

Ping(at)Work / Technical description

Ping(at)Work

Architecture, run flow, validation, data model, HTTP interface and security of Ping(at)Work, network PING templates with a web GUI.

Version 1.0.0Revised 2026-10-07Web GUI and worker

1. Purpose

Ping(at)Work is a web application for defining templates for network PING and running them manually or on a schedule. Results are stored with a status (OK, warning, critical, error) calculated from latency and packet loss thresholds, and status changes can be notified by e-mail or webhook.

The solution is delivered as an offline package for Rocky Linux 9 (x86_64) and runs the same way on a physical server, VMware and Proxmox VE.

2. Architecture

  Browser ──HTTPS:443──▶ Caddy (pingforge-caddy)
  (HTTP:80 → 301 HTTPS)       │  reverse_proxy, TLS, HSTS
                              ▼
                    gunicorn + Flask (pingforge-web)       127.0.0.1:8000
                      │  templates, users, queue, views
                      ▼
               SQLite  /var/lib/pingforge/pingforge.db  (WAL)
                      ▲
                      │  reads the queue, schedules, stores results
             Worker (pingforge-worker) ──▶ /usr/bin/ping (CAP_NET_RAW)
                      │
                      └──▶ SMTP / webhook on status change
Table 1. Components
Component Technology Responsibility

pingforge-caddy

Caddy 2.11.7 (static Go binary)

TLS termination, HTTP→HTTPS, security headers, compression, proxy to gunicorn

pingforge-web

Python 3.9, Flask 3.1, gunicorn 23 (2 processes × 4 threads)

User interface, login, template CRUD, queues runs, shows results, documents

pingforge-worker

Python 3.9, one process with threads

Executes the queue, schedules templates, runs ping in parallel, evaluates and stores, sends notifications, deletes old history

Database

SQLite 3 in WAL mode

Templates, targets, runs, results, users, login failures, key/value (worker heartbeat)

The split between web and worker is deliberate: the web process never starts subprocesses and needs no extra privileges. Only the worker has CAP_NET_RAW.

3. Run flow

  1. The user presses Run or Save and run. The web process creates a row in runs with state = queued and sends the user to the run page.

  2. The worker reads the queue every 2 seconds and starts one thread per run. state becomes running.

  3. For each target and IP version an argument list is built (see Command building), and ping runs in parallel with up to max_parallel processes at once.

  4. The output is parsed (sent, received, loss, min/avg/max/mdev). Each result gets a status from the thresholds. The run’s status is the worst result.

  5. The results are stored in one transaction. The template gets last_status and last_run_at.

  6. If the status has changed since the previous run, a notification is sent.

  7. The run page polls /api/runs/<id> every 1.5 seconds and reloads when the run is finished.

Scheduled templates: every 15 seconds the worker queues the enabled templates where last_run_at + schedule_minutes has passed and that have no run queued or running already.

3.1. Command building

/usr/bin/ping -<4|6> -n -c <count> -i <interval> -s <size> -W <timeout> -w <deadline>
              [-t <ttl>] [-M do] [-Q <dscp<<2>] [-I <interface>] -- <host>
  • deadline = ceil(count × interval + timeout) + 1 seconds. The process is killed after deadline + 10 seconds.

  • -Q gets the full TOS byte, i.e. the DSCP value shifted two bits (EF 46 → 184).

  • The command runs with subprocess.run(list), without a shell, with LC_ALL=C and a minimal PATH.

3.2. Status rules

Status Rule (first match wins)

Error

ping produced no statistics (unknown host name, invalid interface, program error)

Critical

0 replies, or loss ≥ critical loss threshold, or average latency ≥ critical latency threshold

Warning

loss ≥ warning loss threshold, or average latency ≥ warning latency threshold

OK

otherwise

A notification is sent when the status changes. The very first run only notifies if it is not OK.

4. Validation

Field Rule

Target

IPv4/IPv6 (ipaddress), RFC 1123 host name (labels 1–63 characters, no leading/trailing -, ≤ 253 characters), or CIDR expanded to host addresses. At most max_targets (256) in total. Duplicates are removed

Count

1–1000

Interval

0.2–60 s (comma and point accepted)

Size

0–65507 bytes

Timeout

1–60 s

TTL

1–255 or empty

Interface

^[A-Za-z0-9][A-Za-z0-9_.:@%-]{0,63}$ or empty

DSCP

0, 8, 10, 18, 26, 34, 40, 46, 48

Schedule

0, 5, 15, 60 or custom 1–10080 min

Thresholds

Numbers ≥ 0 (loss ≤ 100). Warning ≤ critical

Notify

E-mail addresses

5. Data model

users(id, username UNIQUE, pw_hash, created_at)
templates(id, name, description, ip_version, count, interval, size, timeout, ttl,
          interface, dscp, df, schedule_minutes, warn_latency, crit_latency,
          warn_loss, crit_loss, notify, enabled, last_status, last_run_at,
          created_at, updated_at)
targets(id, template_id → templates ON DELETE CASCADE, host, label, position)
runs(id, template_id → templates ON DELETE SET NULL, template_name, trigger,
     requested_by, state, status, queued_at, started_at, finished_at, message)
results(id, run_id → runs ON DELETE CASCADE, host, label, ip_version, command,
        sent, received, loss, rtt_min, rtt_avg, rtt_max, rtt_mdev, status,
        output, exit_code)
login_failures(key, ts)
kv(key PRIMARY KEY, value)            -- worker_heartbeat

Timestamps are stored as ISO 8601 in UTC and shown in the server’s local time zone. PRAGMA user_version holds the schema version (1).

6. HTTP interface

Method Path Function

GET/POST

/login

Login

POST

/logout

Logout

GET

/templates

Template overview

GET

/templates/new, /templates/<id>

New / edit template, with the last run

POST

/templates/save, /templates/<id>/save

Save (action=save_run saves and runs)

POST

/templates/<id>/run | /duplicate | /delete

Run, duplicate, delete

GET

/runs, /runs/<id>

History and details

GET

/runs/<id>/export.csv

Results as CSV

GET

/api/runs/<id>

JSON {id, state, status} for refreshing the page

GET

/settings

Settings and system status

POST

/settings/password, /settings/import

Change password, import templates

GET

/settings/export.json

Export templates

GET

/lang/<en|no>

Switch language (cookie lang)

GET

/docs/<manual|licenses|notices>

Documents (public)

GET

/healthz

Health check {status, version} (public)

Every POST requires a CSRF token. All pages except login, documents and the health check require login.

6.1. Template export format

{
  "format": "pingforge-templates",
  "version": 1,
  "exported": "2026-10-07T17:43:00+00:00",
  "templates": [
    {
      "name": "Core routers – latency", "description": "", "ip_version": "4",
      "count": 10, "interval": 0.2, "size": 56, "timeout": 1, "ttl": 64,
      "interface": "", "dscp": 46, "df": true, "schedule_minutes": 5,
      "warn_latency": 30.0, "crit_latency": 80.0, "warn_loss": 1.0, "crit_loss": 5.0,
      "notify": "noc@example.com", "enabled": true,
      "targets": [ { "host": "10.10.0.1", "label": "core-osl-01" } ]
    }
  ]
}

6.2. Webhook

POST with Content-Type: application/json:

{ "text": "[PingForge] CRITICAL: WAN links ...", "template": "WAN links", "template_id": 3,
  "run_id": 42, "status": "critical", "previous_status": "ok",
  "results": [ { "host": "192.0.2.10", "label": "wan-b", "ip_version": "4", "status": "critical",
                 "loss": 100.0, "rtt_min": null, "rtt_avg": null, "rtt_max": null, "rtt_mdev": null } ] }

The text field makes incoming webhooks in Slack, Mattermost and Rocket.Chat show the message directly.

7. User interface

  • Server-rendered HTML (Jinja2) that works without JavaScript. static/js/app.js adds dynamic target rows, a live command preview, copying, search, theme switching and refreshing of the run page.

  • Language: all strings live in i18n.py as pairs (English, Norwegian). The choice is stored in the lang cookie. default_language sets the default. The user manual and the licenses page follow the chosen language.

  • Dark/light mode: CSS variables under [data-theme="dark"] and [data-theme="light"]. The choice is stored in the theme cookie so the page renders in the right theme without flashing.

  • Fonts: IBM Plex Sans and Mono, served locally (woff2). No external resources are loaded.

  • Accessibility: semantic buttons and form controls, visible focus, "skip to content", status as text (not only colour), contrast ≥ 4.5:1, works on mobile from 360 px.

8. Security

Measure Implementation

Transport

Caddy with TLS 1.2/1.3, HSTS, HTTP→HTTPS. Self-signed RSA 3072 certificate at installation

Login

Werkzeug scrypt hash. Lockout after 5 failures per user / 20 per IP for 15 min. Session 12 h

Session

Signed cookie (itsdangerous) with Secure, HttpOnly, SameSite=Lax

CSRF

Per-session token on every POST

Headers

CSP default-src 'self' (no inline script or style), X-Frame-Options: DENY, nosniff, Referrer-Policy: same-origin, Cache-Control: no-store

Commands

No shell, validated arguments, -- before the target

Processes

Dedicated system users. systemd hardening: NoNewPrivileges, ProtectSystem=strict, ProtectHome, PrivateTmp, PrivateDevices, CapabilityBoundingSet (web: none, worker: CAP_NET_RAW, Caddy: CAP_NET_BIND_SERVICE)

Network

Only Caddy listens externally. Caddy’s admin API is turned off

9. Files and permissions

Path Owner / mode Content

/opt/pingforge/app/

root, 0644/0755

The pingforge Python package

/opt/pingforge/venv/

root

Python environment (from wheels)

/opt/pingforge/bin/caddy

root, 0755

Caddy

/opt/pingforge/Dokumenter/

root

Documentation (shown in the app)

/etc/pingforge/pingforge.conf

root:pingforge 0640

Configuration and secrets

/etc/pingforge/Caddyfile

root 0644

Proxy configuration

/etc/pingforge/tls/

root:pingforge-caddy 0750

Certificate (0644) and key (0640)

/var/lib/pingforge/

pingforge 0750

Database

/var/lib/pingforge-caddy/

pingforge-caddy 0750

Caddy’s working data

/etc/systemd/system/pingforge-*.service

root 0644

Services

/usr/local/bin/pingforge-admin

root 0755

Administration

10. Capacity

  • One run uses at most max_parallel (16) ping processes. Several templates can run at once.

  • Duration per target ≈ count × interval seconds (5 × 1 s = 5 s). 256 targets with 16 in parallel take about 16 × the duration of one target.

  • Database growth ≈ 1–3 kB per result (including raw output). 50 targets every 5 minutes for 90 days is about 1.3 million results, in the order of 2–4 GB. Lower retention_days for large volumes.

11. Building and packaging

build.sh assembles the package from the source code and vendor/:

  • vendor/wheels/ – wheels for CPython 3.9 manylinux x86_64 (pinned in requirements.txt) and pip.

  • vendor/caddy/ – the Caddy binary and its license (SHA-512 verified on download).

  • vendor/fonts/ – the OFL licenses for IBM Plex.

The result is dist/pingforge-<version>-el9-x86_64.tar.gz with SHA256SUMS inside and a .sha256 file next to it. scripts/gen_notices.py builds the third-party notices and scripts/gen_inventory.py builds third_party_inventory.json from the same files; build.sh runs both.

Package contents
pingforge-1.0.0-el9-x86_64/
├── install.sh  uninstall.sh  VERSION  LICENSE  README.md  requirements.txt  SHA256SUMS
├── third_party_inventory.json
├── app/pingforge/        the Python package (app, worker, cli, templates, static)
├── wheels/               13 wheel files
├── caddy/                caddy, LICENSE
├── config/               Caddyfile, pingforge.conf.example
├── systemd/              pingforge-web/-worker/-caddy.service
└── Dokumenter/           all documentation

12. Source code

File Content

app.py

Flask application: routes, login, CSRF, headers, document view

worker.py

Queue, scheduling, execution, cleanup

runner.py

Building the ping command, running it, parsing and status rules

validation.py

Validation of forms and imports, CIDR expansion

db.py

Schema and database functions

notify.py

E-mail and webhook

i18n.py

English and Norwegian strings

cli.py

pingforge-admin

config.py

Reading pingforge.conf

templates/

Jinja2 templates

static/

CSS, JavaScript, fonts, icon

Screenshots, installation and example templates: Back to the front page