@@ -255,6 +255,14 @@ The `keydb.py` script provides comprehensive database management:
255255./keydb.py setpass PORT2 NewPassPhrase # Change passphrase
256256./keydb.py setport1 PORT2 NewPORT1 # Change user port
257257
258+ # Reset signing replay-protection timestamp (e.g. after clock skew)
259+ ./keydb.py resettimestamp PORT2
260+
261+ # Flag bits (used by the web admin UI to mark admins)
262+ ./keydb.py setflag PORT2 admin # Grant admin to entry
263+ ./keydb.py clearflag PORT2 admin # Revoke admin from entry
264+ ./keydb.py flags PORT2 # Show flags currently set
265+
258266# Examples:
259267./keydb.py add 10006 10007 ' Engineering' SecurePass123
260268./keydb.py setname 10007 ' QA Team'
@@ -275,6 +283,119 @@ The `keydb.py` script provides comprehensive database management:
275283- ** Firewall** : Configure firewall rules to allow only necessary ports
276284- ** Access Control** : Limit access to the server and keydb.py script
277285
286+ ## Web Admin UI
287+
288+ The ` webadmin/ ` directory contains a small Flask app that lets users
289+ manage their own entry through the browser, and lets users with the
290+ ` admin ` flag manage every entry. It writes to the same ` keys.tdb ` the
291+ running ` udpproxy ` reads, so changes go live within ~ 5 seconds with no
292+ proxy restart.
293+
294+ ### Roles and login
295+
296+ - Anyone can log in by entering ** either ` port1 ` or ` port2 ` ** plus the
297+ passphrase set with ` keydb.py ` .
298+ - Users with ` KEY_FLAG_ADMIN ` set on their entry get the admin UI (list
299+ all entries, edit any, grant/revoke admin, add/delete entries).
300+ - Everyone else gets the self-service UI (rename their own entry, rotate
301+ their own passphrase, reset their signing timestamp).
302+
303+ ### Install dependencies
304+
305+ ``` bash
306+ source venv/bin/activate
307+ pip install Flask Flask-WTF gunicorn
308+ ```
309+
310+ ### Bootstrap the first admin
311+
312+ The web UI has no out-of-band admin; you promote an existing entry to
313+ admin from the CLI on the server:
314+
315+ ``` bash
316+ ./keydb.py setflag 10007 admin
317+ ```
318+
319+ That entry can then log in to the web UI as an admin and grant the flag
320+ to others through the web interface.
321+
322+ ### Run standalone
323+
324+ ``` bash
325+ # Set a stable secret so sessions survive restarts
326+ export WEBADMIN_SECRET_KEY=$( python -c ' import secrets; print(secrets.token_hex(32))' )
327+
328+ # Path to keys.tdb (defaults to ./keys.tdb relative to cwd)
329+ export WEBADMIN_KEYDB_PATH=/path/to/proxy/keys.tdb
330+
331+ # Bind to localhost or a public address. Use --certfile/--keyfile for TLS.
332+ gunicorn -w 2 -b 127.0.0.1:8080 webadmin.wsgi:application
333+ ```
334+
335+ For local HTTP-only development, also set ` WEBADMIN_INSECURE_COOKIES=1 `
336+ so the session cookie is sent without the ` Secure ` flag. ** Do not set
337+ this in production.**
338+
339+ For local development from ` test/ ` (or any directory containing a
340+ ` keys.tdb ` ), there's a helper that wires the env vars up for you:
341+
342+ ``` bash
343+ cd test
344+ ../scripts/run_webui.sh # http://127.0.0.1:8080
345+ WEBADMIN_PORT=9000 ../scripts/run_webui.sh
346+ WEBADMIN_TLS=1 ../scripts/run_webui.sh # uses fullchain.pem / privkey.pem in cwd
347+ ```
348+
349+ It uses ` gunicorn ` if available and falls back to Flask's dev server
350+ otherwise.
351+
352+ ### Run behind Apache (` ProxyPass ` )
353+
354+ The intended deployment is for the web UI to listen on
355+ ` 127.0.0.1:8080 ` and have an existing Apache vhost forward an
356+ externally-managed URL to it. Apache terminates TLS; the app trusts
357+ ` X-Forwarded-* ` headers from Apache when ` WEBADMIN_BEHIND_PROXY=1 ` :
358+
359+ ``` apache
360+ <VirtualHost *:443>
361+ ServerName support.example.org
362+ SSLEngine on
363+ SSLCertificateFile /etc/letsencrypt/live/support.example.org/fullchain.pem
364+ SSLCertificateKeyFile /etc/letsencrypt/live/support.example.org/privkey.pem
365+
366+ ProxyPreserveHost On
367+ ProxyPass /admin/ http://127.0.0.1:8080/
368+ ProxyPassReverse /admin/ http://127.0.0.1:8080/
369+ RequestHeader set X-Forwarded-Prefix /admin
370+ RequestHeader set X-Forwarded-Proto https
371+ </VirtualHost>
372+ ```
373+
374+ Required Apache modules: ` mod_proxy ` , ` mod_proxy_http ` , ` mod_headers ` ,
375+ ` mod_ssl ` . Then run gunicorn with the proxy flag set:
376+
377+ ``` bash
378+ export WEBADMIN_BEHIND_PROXY=1
379+ export WEBADMIN_SECRET_KEY=...
380+ export WEBADMIN_KEYDB_PATH=/path/to/proxy/keys.tdb
381+ gunicorn -w 2 -b 127.0.0.1:8080 webadmin.wsgi:application
382+ ```
383+
384+ The browser hits ` https://support.example.org/admin/ ` and Apache forwards
385+ to the local gunicorn. URL-building inside the app uses
386+ ` X-Forwarded-Prefix ` so links resolve correctly under ` /admin/ ` .
387+
388+ ### Configuration reference
389+
390+ All settings can be overridden via environment variables:
391+
392+ | Variable | Default | Purpose |
393+ | ---| ---| ---|
394+ | ` WEBADMIN_SECRET_KEY ` | random per process | Flask session + CSRF signing key. Set a stable value in production. |
395+ | ` WEBADMIN_KEYDB_PATH ` | ` keys.tdb ` (cwd-relative) | Path to the TDB file. |
396+ | ` WEBADMIN_BEHIND_PROXY ` | unset | Set to ` 1 ` when fronted by Apache to honour ` X-Forwarded-* ` headers. |
397+ | ` WEBADMIN_INSECURE_COOKIES ` | unset | Set to ` 1 ` only for local HTTP dev; allows session cookie without ` Secure ` flag. |
398+
278399## Troubleshooting
279400
280401### Common Issues
@@ -324,20 +445,33 @@ journalctl -f | grep udpproxy
324445
325446## Testing
326447
327- ### Automated CI Testing
328-
329- UDPProxy includes comprehensive CI testing to validate UDP and TCP connection functionality:
448+ The test suite covers UDP/TCP connection scenarios, the ` keydb.py ` CLI,
449+ and the web admin UI (auth, role guards, CSRF, concurrent writes).
330450
331451``` bash
332- # Run all tests locally
333- ./run_tests.sh
452+ # Run everything (builds udpproxy, runs all three phases)
453+ ./scripts/ run_tests.sh
334454
335- # Run specific test suites
336- source venv/bin/activate
337- pytest tests/test_connections.py -v
338- pytest tests/test_authentication.py -v
455+ # Run in parallel via pytest-xdist (-j N workers, each with its own
456+ # tmpdir + port pair so they don't collide)
457+ ./scripts/run_tests.sh -j 4
458+
459+ # List every test without running anything
460+ ./scripts/run_tests.py --list
461+
462+ # Run specific tests (any pytest selector works). --no-build skips the
463+ # make step for fast iteration.
464+ ./scripts/run_tests.py --no-build tests/webadmin/
465+ ./scripts/run_tests.py --no-build tests/test_connections.py::TestUDPConnections
466+ ./scripts/run_tests.py --no-build -j 4 tests/webadmin/test_admin.py::TestAdminEdit::test_cannot_revoke_last_admin
339467```
340468
469+ When invoked with no test selectors, the runner builds and then runs
470+ three separate pytest invocations (connection tests, authentication
471+ tests, webadmin tests) — they're kept separate because each phase has
472+ different cwd / ` keys.tdb ` / process expectations. With selectors, it
473+ runs one pytest invocation against exactly what you passed.
474+
341475## Contributing
342476
3434771 . ** Code Style** : Follow existing C++ and Python conventions
0 commit comments