feature: Added an internally only exposed status web service.
Release / release (push) Successful in 17s

This commit is contained in:
slawek committed 2026-10-23 19:46:26 +02:00
1 parent 5402256ef6
commit 7163f167e0
10 files changed
+593

No files matched your search

+71
View File
@@ -115,6 +115,9 @@ must have the type shown below; an unknown section or setting is an error.
| `wireguard` | `listen_port` | integer | `51820` | UDP port |
| `docker` | `mode` | string | `auto` | `auto` secures Docker when its service is installed, `on` requires it, `off` removes the rules |
| `docker` | `exposed_ports` | list | | TCP ports and port ranges (`8000:8100`) reachable from the external interface; at most 15, a range counting as two |
| `web` | `enabled` | boolean | `true` | The status web service on the internal address |
| `web` | `port` | integer | `8080` | TCP port of the status web service |
| `web` | `jwt_secret` | string | | Secret, at least 32 characters, that signs the tokens for `/config`; without it `/config` is not served |
A minimal site-to-site configuration in `auto` mode:
@@ -321,6 +324,74 @@ EXTRA_LOCAL_TS = '192.0.2.53/32'
Set `general.platform` to `example` and restart the service. The debconf question offers only the
modules shipped with the package; the configuration file accepts any module installed.
## Status web service
The `vpn-router-web` service shows the state of the router on its internal address and
`web.port` (8080 by default). It uses only the Python standard library, serves HTTP without
authentication, and shows no configuration: no key, identity, address or subnet.
| Page | Content |
| --- | --- |
| `/` | The status for people: plain text for `curl` and `wget`, a page styled with Tailwind CSS for a browser (the style sheet is loaded from a CDN, so the browser needs internet access; the text is readable without it) |
| `/status` | The same status as JSON |
| `/config` | The configuration with its secrets masked, and what the router resolved and runs, as JSON; needs a token |
The status holds the state of `vpn-router-setup`, of `strongswan` when site-to-site or road-warrior
access is configured, and of `wg-quick@wg0` when WireGuard is; the package version; whether
Docker is installed, and when it is, the state of `docker.service` and of `vpn-router-docker`
(the Docker rules), which do not change `status`; whether the
site-to-site tunnel is established; the number of connected road-warrior (IPsec P2S) clients; and
the WireGuard peers with the age of their last handshake and their traffic. `status` is `ok` when
all these services are active, and `degraded` otherwise.
```bash
curl http://10.0.0.4:8080/
curl http://10.0.0.4:8080/status
```
### The /config page
`/config` shows what `/` and `/status` leave out, for a client such as manage-ui:
| Key | Content |
| --- | --- |
| `configuration` | `vpn-router.json` with the defaults applied and the secrets masked (`psk`, `jwt_secret`, and any option named like a password, token or key) |
| `network` | The external and internal interface, their addresses, the internal gateway, the local and remote IKE identities and subnets, and the IPv4 routes |
| `ipsec` | The output of `swanctl --list-conns` and `swanctl --list-sas`, as lists of lines |
| `wireguard` | The public key and port of `wg0`, and each peer's public key, endpoint, allowed IPs, handshake age and traffic; no private or preshared key |
| `docker` | `docker.exposed_ports` and the rules of the `VPN-ROUTER-DOCKER` chain |
The page needs a JWT in an `Authorization: Bearer` header, signed with HS256 by `web.jwt_secret`.
The token must have an `exp` claim, not more than ten minutes from now (a minute of clock
difference is allowed); `nbf` is checked when present. Other algorithms are refused, with 401 and
`WWW-Authenticate: Bearer`. While `web.jwt_secret` is not set the page is not served (404). The
token and the page travel over plain HTTP on the internal network, and the token can be used
until it expires, so keep the lifetime short. A client makes a token from the secret with only
HMAC-SHA256; with Python:
```python
import base64, hashlib, hmac, json, time
def b64(data):
return base64.urlsafe_b64encode(data).rstrip(b'=').decode()
secret = 'the web.jwt_secret value'
head = b64(json.dumps({'alg': 'HS256', 'typ': 'JWT'}).encode())
body = b64(json.dumps({'exp': int(time.time()) + 300}).encode())
sign = b64(hmac.new(secret.encode(), f'{head}.{body}'.encode(), hashlib.sha256).digest())
print(f'{head}.{body}.{sign}')
```
```bash
curl -H "Authorization: Bearer $TOKEN" http://10.0.0.4:8080/config
```
`vpn-router-setup` opens `web.port` on the internal interface in the firewall, writes the address,
the port, the secret and the facts for `/config` to `/run/vpn-router/web.json` (readable by root
only) and restarts the service, so a changed `web` section
applies with `systemctl restart vpn-router-setup`. The service ends without an error when
`web.enabled` is `false`, and keeps trying to start until the router has an internal address.
## Removal
`apt-get remove` stops `vpn-router-docker`, which removes the Docker rules. It terminates the