Compare commits

..
3 Commits
9 changed files with 88 additions and 10 deletions

No files matched your search

+2 -2
View File
@@ -394,8 +394,8 @@ failure, then listens. `vpnrouter_auth` turns every failure of the database into
psycopg (Ubuntu ships only its pure-Python implementation) finds libpq through the dynamic linker psycopg (Ubuntu ships only its pure-Python implementation) finds libpq through the dynamic linker
cache, which dpkg refreshes with its deferred `ldconfig` only at the end of an installation, so a cache, which dpkg refreshes with its deferred `ldconfig` only at the end of an installation, so a
service started during the installation cannot import it yet; a failed import cannot be repeated service started during the installation cannot import it yet; a failed import cannot be repeated
in the same process either, as psycopg caches the lookup. The services therefore import psycopg in the same process either, as psycopg caches the lookup. The services and `vpn-router-setup`
plainly, and their units wait instead: `ExecStartPre=wait-for-libpq`, a shell script of each therefore import psycopg plainly, and their units wait instead: `ExecStartPre=wait-for-libpq`, a shell script of each
package, waits until `ldconfig -p` lists `libpq.so.5`, within `TimeoutStartSec=5min`. The package, waits until `ldconfig -p` lists `libpq.so.5`, within `TimeoutStartSec=5min`. The
maintainer scripts start the units with `systemctl --no-block` (debhelper's start is turned off), maintainer scripts start the units with `systemctl --no-block` (debhelper's start is turned off),
because a start that waited would hold dpkg before its `ldconfig`. because a start that waited would hold dpkg before its `ldconfig`.
+13 -1
View File
@@ -1007,7 +1007,7 @@ curl http://10.0.0.4:7081/status
### Tokens and signing in ### Tokens and signing in
`/config`, `/users`, `/service-accounts`, `/groups`, `/site-to-site`, `/backup`, `/remote-access`, `/ca`, `/logs`, `/sites`, `/dns`, `/shell` and `/account` need a JWT in an `Authorization: Bearer` header, signed with `/config`, `/metrics/history`, `/metrics/settings`, `/users`, `/service-accounts`, `/groups`, `/site-to-site`, `/backup`, `/remote-access`, `/ca`, `/logs`, `/sites`, `/dns`, `/shell` and `/account` need a JWT in an `Authorization: Bearer` header, signed with
HS256 by the key the web service makes on its first run, `/etc/vpn-router/web.key` (root only). HS256 by the key the web service makes on its first run, `/etc/vpn-router/web.key` (root only).
The key never leaves the router; while it does not exist these paths and `/auth` are not served The key never leaves the router; while it does not exist these paths and `/auth` are not served
(404). A missing, expired or wrongly signed token, or one of another algorithm, gets 401 with (404). A missing, expired or wrongly signed token, or one of another algorithm, gets 401 with
@@ -1332,6 +1332,18 @@ The answer of `GET /backup` holds:
A change is made, checked and applied as for `/site-to-site`. A change is made, checked and applied as for `/site-to-site`.
### The /metrics/settings API
`/metrics/settings` shows and changes the settings of the metrics service, with the same token as
`/users`.
| Request | Body | Answer |
| --- | --- | --- |
| `GET /metrics/settings` | | `{"settings"}`: the `metrics` section without `otlp_password`, with `otlp_password_set` |
| `PUT /metrics/settings` | any of the settings of the `metrics` section; an empty or absent `otlp_password` keeps the secret, and an empty `otlp_username` removes it | `{"settings"}` |
A change is made, checked and applied as for `/site-to-site`.
### The /remote-access API ### The /remote-access API
`/remote-access` shows and changes the remote access systems, IKEv2 (`p2s`), `/remote-access` shows and changes the remote access systems, IKEv2 (`p2s`),
+22
View File
@@ -1,3 +1,25 @@
vpn-router (0.34.0-1) unstable; urgency=medium
feature: the web service shows and changes the settings of the metrics service at /metrics/settings, and OpenVPN starts on a new router, as setup installs CRLs that revoke nothing.
* GET and PUT /metrics/settings show and change the metrics section;
otlp_password is never shown, an empty one keeps it, and an empty
otlp_username removes it.
* A CRL without revoked certificates is installed for strongSwan and
OpenVPN.
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Wed, 28 Oct 2026 22:00:00 +0100
vpn-router (0.33.3-1) unstable; urgency=medium
fix: vpn-router-setup runs on a new installation once libpq can be found.
* The vpn-router-setup unit waits, with wait-for-libpq, until libpq is in
the linker cache before it runs setup.
* The package starts vpn-router-setup without waiting for it.
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Wed, 28 Oct 2026 21:00:00 +0100
vpn-router (0.33.2-1) unstable; urgency=medium vpn-router (0.33.2-1) unstable; urgency=medium
fix: the router's IPsec remote access is called IKEv2 instead of road-warrior. fix: the router's IPsec remote access is called IKEv2 instead of road-warrior.
+4 -2
View File
@@ -71,9 +71,11 @@ case "$1" in
# setting the forward policy all belong to vpn-router-setup, which does # setting the forward policy all belong to vpn-router-setup, which does
# them only once the configuration says this host is a router. # them only once the configuration says this host is a router.
# The web service waits for libpq in the linker cache, which dpkg # The setup and web services wait for libpq in the linker cache, which
# refreshes after this script, so it is started without waiting for it. # dpkg refreshes after this script, so they are started without waiting
# for them.
if [ -d /run/systemd/system ]; then if [ -d /run/systemd/system ]; then
systemctl --no-block restart vpn-router-setup.service >/dev/null 2>&1 || true
systemctl --no-block restart vpn-router-web.service >/dev/null 2>&1 || true systemctl --no-block restart vpn-router-web.service >/dev/null 2>&1 || true
fi fi
;; ;;
+1 -1
View File
@@ -3,7 +3,7 @@
dh $@ dh $@
override_dh_installsystemd: override_dh_installsystemd:
dh_installsystemd --name=vpn-router-setup dh_installsystemd --name=vpn-router-setup --no-start
dh_installsystemd --name=vpn-router-docker dh_installsystemd --name=vpn-router-docker
dh_installsystemd --name=vpn-router-web --no-start dh_installsystemd --name=vpn-router-web --no-start
dh_installsystemd --name=vpn-router-metrics --no-enable --no-start dh_installsystemd --name=vpn-router-metrics --no-enable --no-start
@@ -6,6 +6,7 @@ After=network-online.target docker.service postgresql.service
[Service] [Service]
Type=oneshot Type=oneshot
ExecStartPre=/usr/lib/vpn-router/wait-for-libpq
ExecStart=/usr/lib/vpn-router/setup ExecStart=/usr/lib/vpn-router/setup
RemainAfterExit=yes RemainAfterExit=yes
@@ -722,7 +722,7 @@ def install_crls(connection):
crls = {} crls = {}
for name, file in SWANCTL_CA_FILES.items(): for name, file in SWANCTL_CA_FILES.items():
entry = authority(connection, name) entry = authority(connection, name)
crls[name] = entry['crl'].public_bytes(serialization.Encoding.PEM) if entry and entry['crl'] else None crls[name] = entry['crl'].public_bytes(serialization.Encoding.PEM) if entry and entry['crl'] is not None else None
changed |= write(X509CRL_DIR / file, crls[name], 0o644) if crls[name] else remove(X509CRL_DIR / file) changed |= write(X509CRL_DIR / file, crls[name], 0o644) if crls[name] else remove(X509CRL_DIR / file)
if crls['root'] and crls['p2s']: if crls['root'] and crls['p2s']:
changed |= write(OPENVPN_CRL, crls['root'] + crls['p2s'], 0o644) changed |= write(OPENVPN_CRL, crls['root'] + crls['p2s'], 0o644)
@@ -1,6 +1,7 @@
#!/bin/sh #!/bin/sh
# Wait until libpq is in the dynamic linker cache, where psycopg looks it up. # Wait until libpq is in the dynamic linker cache, where psycopg looks it up.
# ExecStartPre= of vpn-router-web.service; TimeoutStartSec= of the unit bounds the wait. # ExecStartPre= of vpn-router-setup.service, vpn-router-web.service and vpn-router-metrics.service;
# TimeoutStartSec= of the unit bounds the wait.
reported= reported=
until ldconfig -p | grep -q '^[[:space:]]*libpq\.so\.5 '; do until ldconfig -p | grep -q '^[[:space:]]*libpq\.so\.5 '; do
+42 -2
View File
@@ -47,6 +47,9 @@ Pages:
/metrics/history /metrics/history
the metrics the metrics service keeps, for a view: hour, day, month or year (see the metrics the metrics service keeps, for a view: hour, day, month or year (see
metrics_history()) metrics_history())
/metrics/settings
the settings of the metrics service: show and change, applied by vpn-router-setup (see
metrics_settings())
/logs the last lines of the journal of the router's units and of the files web.log_files lists /logs the last lines of the journal of the router's units and of the files web.log_files lists
(see logs()) (see logs())
/sites the web sites Caddy serves: the settings of sites, list, add, change and remove (see sites()), /sites the web sites Caddy serves: the settings of sites, list, add, change and remove (see sites()),
@@ -62,7 +65,7 @@ Pages:
loopback address, where Caddy passes the requests of the HTTPS site; POST /oidc/login counts loopback address, where Caddy passes the requests of the HTTPS site; POST /oidc/login counts
towards the limit of /auth calls towards the limit of /auth calls
/status/full, /config, /metrics/history, /postgresql, /users, /service-accounts, /groups, /site-to-site, /backup, /remote-access, /ca, /logs, /sites, /dns, /shell and /account need an HS256 JWT in an Authorization: Bearer header signed /status/full, /config, /metrics/history, /metrics/settings, /postgresql, /users, /service-accounts, /groups, /site-to-site, /backup, /remote-access, /ca, /logs, /sites, /dns, /shell and /account need an HS256 JWT in an Authorization: Bearer header signed
with vpnrouter_auth.WEB_KEY (see vpnrouter_auth.verify). A token of /auth names a user in sub; with vpnrouter_auth.WEB_KEY (see vpnrouter_auth.verify). A token of /auth names a user in sub;
/account serves that user, and the other paths need a member of vpnrouter_users.ADMIN_GROUP. A /account serves that user, and the other paths need a member of vpnrouter_users.ADMIN_GROUP. A
token of vpn-router-token has the admin claim and names no user; it may call every path but token of vpn-router-token has the admin claim and names no user; it may call every path but
@@ -1351,6 +1354,39 @@ def metrics_history(view):
return 500, {'error': str(exc.code)} return 500, {'error': str(exc.code)}
def metrics_shown(settings):
"""The metrics section as /metrics/settings shows it: its settings but otlp_password, with otlp_password_set."""
return {**{option: value for option, value in settings.items() if option != 'otlp_password'},
'otlp_password_set': bool(settings['otlp_password'])}
def metrics_settings(method, body):
"""Answer a /metrics/settings request: the HTTP status and the JSON document.
GET /metrics/settings {"settings"}: the metrics section without otlp_password, with otlp_password_set
PUT /metrics/settings change any of the settings of the metrics section; an empty or absent otlp_password keeps
the current secret, and an empty otlp_username removes it
A change is applied by restarting vpn-router-setup without waiting for it.
"""
if body == 'invalid' or (body is not None and not isinstance(body, dict)):
return 400, {'error': 'the body must be a JSON object'}
if method in ('GET', 'HEAD'):
try:
return 200, {'settings': metrics_shown(vpnrouter.load_config()['metrics'])}
except SystemExit as exc:
return 500, {'error': str(exc.code)}
if method != 'PUT':
return 404, {'error': f'no route {method} /metrics/settings'}
body = dict(body or {})
if not body.get('otlp_password'):
body.pop('otlp_password', None)
if body.get('otlp_username') == '':
body['otlp_password'] = ''
changed = section_change('metrics', body)
return changed if isinstance(changed, tuple) else (200, {'settings': metrics_shown(changed)})
def changes_admins(connection, method, parts, body): def changes_admins(connection, method, parts, body):
"""Whether a /users, /service-accounts or /groups request creates, renames to, changes or removes ADMIN_GROUP, or """Whether a /users, /service-accounts or /groups request creates, renames to, changes or removes ADMIN_GROUP, or
removes a member of it.""" removes a member of it."""
@@ -1819,7 +1855,8 @@ class Handler(BaseHTTPRequestHandler):
directory = next((name for name in ('users', 'service-accounts', 'groups', 'account', 'site-to-site', 'backup', directory = next((name for name in ('users', 'service-accounts', 'groups', 'account', 'site-to-site', 'backup',
'ca', 'shell', 'remote-access', 'logs', 'sites', 'dns') 'ca', 'shell', 'remote-access', 'logs', 'sites', 'dns')
if path == f'/{name}' or path.startswith(f'/{name}/')), None) if path == f'/{name}' or path.startswith(f'/{name}/')), None)
protected = path in ('/status/full', '/config', '/metrics/history', '/postgresql') or directory is not None protected = path in ('/status/full', '/config', '/metrics/history', '/metrics/settings', '/postgresql') or \
directory is not None
signing = path in ('/auth', '/auth/passkey', '/auth/passkey/complete', '/auth/refresh', '/auth/logout') signing = path in ('/auth', '/auth/passkey', '/auth/passkey/complete', '/auth/refresh', '/auth/logout')
oidc = (path == '/.well-known/openid-configuration' or path.startswith('/oidc/')) and \ oidc = (path == '/.well-known/openid-configuration' or path.startswith('/oidc/')) and \
self.server.server_address[0] == vpnrouter_net.LOOPBACK self.server.server_address[0] == vpnrouter_net.LOOPBACK
@@ -1861,6 +1898,9 @@ class Handler(BaseHTTPRequestHandler):
query = urllib.parse.parse_qs(urllib.parse.urlsplit(self.path).query) query = urllib.parse.parse_qs(urllib.parse.urlsplit(self.path).query)
code, document = metrics_history(query.get('range', ['hour'])[0]) code, document = metrics_history(query.get('range', ['hour'])[0])
body, kind = json.dumps(document).encode() + b'\n', 'application/json' body, kind = json.dumps(document).encode() + b'\n', 'application/json'
elif path == '/metrics/settings':
code, document = metrics_settings(self.command, payload)
body, kind = json.dumps(document, indent=2).encode() + b'\n', 'application/json'
elif directory == 'shell' and (path, self.command) == ('/shell', 'GET'): elif directory == 'shell' and (path, self.command) == ('/shell', 'GET'):
return self.shell(claims.get('sub')) return self.shell(claims.get('sub'))
elif directory == 'shell': elif directory == 'shell':