Compare commits

..
2 Commits
Author SHA1 Message Date
slawek 83c1c50d8d feature: vpn-config-edit shows and changes the router's configuration.
Release / release (push) Successful in 19s
2026-10-25 13:29:12 +01:00
slawek 650896131d fix: web.status_content is basic by default.
Release / release (push) Successful in 19s
2026-10-25 13:09:57 +01:00
8 changed files with 185 additions and 14 deletions

No files matched your search

+3 -3
View File
@@ -44,9 +44,9 @@ address of the internal interface, of the external interface or of both, as `web
unless `web.enabled` is `false` (ADR-7). `/` is for people, in plain text for `curl` and `wget` unless `web.enabled` is `false` (ADR-7). `/` is for people, in plain text for `curl` and `wget`
and as a Tailwind CSS page for a browser, and `/status` is the same status as JSON. Both show as and as a Tailwind CSS page for a browser, and `/status` is the same status as JSON. Both show as
much as `web.status_content` says, since the router is reachable from the internet and the much as `web.status_content` says, since the router is reachable from the internet and the
operator decides how much it tells strangers: `basic` the overall status and the Client IP, operator decides how much it tells strangers: `basic`, the default because it tells the least,
`standard` also the package versions and the number of active and other services, and `full`, the overall status and the Client IP, `standard` also the package versions and the number of
the default, also each service and the state of the site-to-site, road-warrior, OpenVPN and active and other services, and `full` also each service and the state of the site-to-site, road-warrior, OpenVPN and
WireGuard features. The Client IP is the address the request came from, through the trusted WireGuard features. The Client IP is the address the request came from, through the trusted
proxies, so a person can see the address the router sees. Nothing comes from the configuration. proxies, so a person can see the address the router sees. Nothing comes from the configuration.
`/status` names its content, so clients such as manage-ui and the Router Manager show what it `/status` names its content, so clients such as manage-ui and the Router Manager show what it
+21 -3
View File
@@ -128,7 +128,7 @@ must have the type shown below; an unknown section or setting is an error.
| `web` | `port` | integer | `7081` | TCP port of the status web service | | `web` | `port` | integer | `7081` | TCP port of the status web service |
| `web` | `database` | string | | A PostgreSQL connection string for the refresh tokens; without it, the `vpn-router` database on this host that the web service creates | | `web` | `database` | string | | A PostgreSQL connection string for the refresh tokens; without it, the `vpn-router` database on this host that the web service creates |
| `web` | `trusted_proxies` | list | the address the call came in on | Addresses whose `X-Forwarded-For` header names the client for the `/auth` rate limit and the Client IP of the status | | `web` | `trusted_proxies` | list | the address the call came in on | Addresses whose `X-Forwarded-For` header names the client for the `/auth` rate limit and the Client IP of the status |
| `web` | `status_content` | string | `full` | What `/` and `/status` show: `basic`, `standard` or `full` (see Status web service) | | `web` | `status_content` | string | `basic` | What `/` and `/status` show: `basic`, `standard` or `full` (see Status web service) |
| `https` | `enabled` | boolean | `true` | Caddy serves the status web service and the Router Manager over HTTPS for `wan.local_fqdn` | | `https` | `enabled` | boolean | `true` | Caddy serves the status web service and the Router Manager over HTTPS for `wan.local_fqdn` |
| `https` | `email` | string | | The e-mail address of the Let's Encrypt account | | `https` | `email` | string | | The e-mail address of the Let's Encrypt account |
| `https` | `web_port` | integer | `7444` | HTTPS port of the status web service | | `https` | `web_port` | integer | `7444` | HTTPS port of the status web service |
@@ -240,6 +240,24 @@ vpn-router-setup` applies a changed configuration while Docker runs. With `docke
`on`, the service fails when Docker is not installed or its daemon is not running; with `on`, the service fails when Docker is not installed or its daemon is not running; with
`off` it removes the rules. `off` it removes the rules.
## Editing the configuration
`vpn-config-edit`, run as root, shows and changes `/etc/vpn-router/vpn-router.json`:
```sh
vpn-config-edit show
vpn-config-edit set web.status_content standard
vpn-config-edit set web.port 7082
vpn-config-edit set web.trusted_proxies 127.0.0.1,10.0.0.4
```
`show` prints the file as indented JSON and does not change it. `set` takes a setting as
`section.option` and converts the value to the setting's type: a whole number, `true` or
`false`, a list separated by commas (an empty value is an empty list), or a string; an unknown
setting is refused. It checks the whole configuration as `vpn-router-setup` does, and only then
writes the file, indented, with mode 0600, and reminds that `vpn-router-setup` applies it. A
refused request exits with status 2, any other failure with 1.
## Applying changes ## Applying changes
```sh ```sh
@@ -506,9 +524,9 @@ configuration: no key, identity, subnet, or address but the client's. They show
| `status_content` | `/` and `/status` show | | `status_content` | `/` and `/status` show |
|---|---| |---|---|
| `basic` | The overall status (`ok` or `degraded`), the time and the Client IP | | `basic` (default) | The overall status (`ok` or `degraded`), the time and the Client IP |
| `standard` | Also the `vpn-router` version, the `vpn-router-manager` version or that it is not installed, and the number of active services and of services in any other state, without their names | | `standard` | Also the `vpn-router` version, the `vpn-router-manager` version or that it is not installed, and the number of active services and of services in any other state, without their names |
| `full` (default) | Also each service and its state, Docker, and the state of site-to-site, road-warrior, OpenVPN and WireGuard | | `full` | Also each service and its state, Docker, and the state of site-to-site, road-warrior, OpenVPN and WireGuard |
The Client IP is the address the request came from: the last entry of `X-Forwarded-For` for a The Client IP is the address the request came from: the last entry of `X-Forwarded-For` for a
request from one of `web.trusted_proxies`, by default the address the request came in on, so request from one of `web.trusted_proxies`, by default the address the request came in on, so
+23
View File
@@ -1,3 +1,26 @@
vpn-router (0.15.0-1) unstable; urgency=medium
feature: vpn-config-edit shows and changes the router's configuration.
* vpn-config-edit show prints /etc/vpn-router/vpn-router.json as
indented JSON.
* vpn-config-edit set SECTION.OPTION VALUE converts the value to the
setting's type, checks the whole configuration as vpn-router-setup
does, writes the file indented with mode 0600, and reminds that
vpn-router-setup applies it.
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Sun, 25 Oct 2026 18:00:00 +0200
vpn-router (0.14.1-1) unstable; urgency=medium
fix: web.status_content is basic by default.
* Without web.status_content, or with a value that is not allowed, /
and /status show only the overall status, the time and the Client
IP; set it to standard or full to show more.
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Sun, 25 Oct 2026 17:00:00 +0200
vpn-router (0.14.0-1) unstable; urgency=medium vpn-router (0.14.0-1) unstable; urgency=medium
feature: the status page shows the Client IP, and web.status_content sets how much it shows. feature: the status page shows the Client IP, and web.status_content sets how much it shows.
+1
View File
@@ -9,6 +9,7 @@ src/usr/lib/vpn-router/openvpn-same-user usr/lib/vpn-router/
src/usr/sbin/vpn-router-user usr/sbin/ src/usr/sbin/vpn-router-user usr/sbin/
src/usr/sbin/vpn-router-group usr/sbin/ src/usr/sbin/vpn-router-group usr/sbin/
src/usr/sbin/vpn-router-token usr/sbin/ src/usr/sbin/vpn-router-token usr/sbin/
src/usr/sbin/vpn-config-edit usr/sbin/
src/usr/lib/vpn-router/simple-ca usr/lib/vpn-router/ src/usr/lib/vpn-router/simple-ca usr/lib/vpn-router/
src/usr/lib/vpn-router/configure usr/lib/vpn-router/ src/usr/lib/vpn-router/configure usr/lib/vpn-router/
src/usr/lib/vpn-router/docker-rules usr/lib/vpn-router/ src/usr/lib/vpn-router/docker-rules usr/lib/vpn-router/
@@ -55,7 +55,7 @@ SCHEMA = {
'openvpn': {'enabled': False, 'address_pool': '', 'port': 1194, 'protocol': 'udp'}, 'openvpn': {'enabled': False, 'address_pool': '', 'port': 1194, 'protocol': 'udp'},
'docker': {'mode': 'auto', 'exposed_ports': []}, 'docker': {'mode': 'auto', 'exposed_ports': []},
'web': {'enabled': True, 'listen': DEFAULT_LISTEN, 'port': 7081, 'database': '', 'trusted_proxies': [], 'web': {'enabled': True, 'listen': DEFAULT_LISTEN, 'port': 7081, 'database': '', 'trusted_proxies': [],
'status_content': 'full'}, 'status_content': 'basic'},
'https': {'enabled': True, 'email': '', 'web_port': 7444, 'manager_port': 7443}, 'https': {'enabled': True, 'email': '', 'web_port': 7444, 'manager_port': 7443},
} }
+6 -6
View File
@@ -29,10 +29,10 @@ served (404) while the key does not exist. /auth is limited to AUTH_LIMIT calls
address in AUTH_WINDOW seconds; the address of a call from one of web.trusted_proxies, by address in AUTH_WINDOW seconds; the address of a call from one of web.trusted_proxies, by
default the address the call came in on, is the last entry of its X-Forwarded-For header. default the address the call came in on, is the last entry of its X-Forwarded-For header.
/ and /status show only the status, as much as web.status_content says (see shown): basic the / and /status show only the status, as much as web.status_content says (see shown): basic, the
overall status, the time and the client's address (its address as for the rate limit below); default, the overall status, the time and the client's address (its address as for the rate limit
standard also the package version, the installed version of vpn-router-manager (manager_version, below); standard also the package version, the installed version of vpn-router-manager
null when it is not installed) and the number of active and other services; full, the default, (manager_version, null when it is not installed) and the number of active and other services; full
whether each service runs, whether Docker is installed, and the state of site-to-site, road-warrior whether each service runs, whether Docker is installed, and the state of site-to-site, road-warrior
(IPsec P2S), OpenVPN and WireGuard. No configuration data or key is shown, and no address but the (IPsec P2S), OpenVPN and WireGuard. No configuration data or key is shown, and no address but the
client's. client's.
@@ -298,10 +298,10 @@ def shown(status, content, client):
Every content has status, time and client_ip, the client's address, and content, which one it is: basic nothing Every content has status, time and client_ip, the client's address, and content, which one it is: basic nothing
more; standard adds version, manager_version and services, the number of active units and of units in every more; standard adds version, manager_version and services, the number of active units and of units in every
other state; full adds the whole status document of collect. Content that is not one of other state; full adds the whole status document of collect. Content that is not one of
vpnrouter.STATUS_CONTENTS is full. vpnrouter.STATUS_CONTENTS, or not set, is basic.
""" """
if content not in vpnrouter.STATUS_CONTENTS: if content not in vpnrouter.STATUS_CONTENTS:
content = 'full' content = 'basic'
if content == 'full': if content == 'full':
return {**status, 'content': content, 'client_ip': client} return {**status, 'content': content, 'client_ip': client}
document = {'status': status['status'], 'content': content, 'time': status['time'], 'client_ip': client} document = {'status': status['status'], 'content': content, 'time': status['time'], 'client_ip': client}
+129
View File
@@ -0,0 +1,129 @@
#!/usr/bin/python3
"""Show and change the router's configuration, /etc/vpn-router/vpn-router.json.
show prints the file as indented JSON; the file is not changed. set takes a setting as
section.option, such as web.port, and a value, which it converts to the type the package's
schema (vpnrouter.SCHEMA) gives the setting: a whole number, true or false, a list of strings
separated by commas (an empty value is an empty list), or a string. The configuration with the
new value is checked as vpn-router-setup checks it, and only then written, indented, with mode
0600; vpn-router-setup applies it at its next run, which set reminds of. A failure is a message
on standard error; the exit status is 2 for an invalid request and 1 for any other failure.
-h or --help shows the options.
vpn-config-edit show
vpn-config-edit set SECTION.OPTION VALUE
"""
import argparse
import json
import os
import sys
import tempfile
sys.path.insert(0, '/usr/lib/vpn-router')
import vpnrouter
class Failure(Exception):
"""A request that cannot be done; status is 2 invalid, 1 other."""
def __init__(self, message, status=1):
super().__init__(message)
self.status = status
class Parser(argparse.ArgumentParser):
"""An argument parser whose errors are Failures with status 2."""
def error(self, message):
raise Failure(f'{message}; see {self.prog} --help', 2)
def read():
"""The settings in the configuration file, without the defaults; {} when the file does not exist."""
try:
data = json.loads(vpnrouter.CONFIG_FILE.read_text(encoding='utf-8'))
except FileNotFoundError:
return {}
except ValueError as exc:
raise Failure(f'cannot parse {vpnrouter.CONFIG_FILE}: {exc}')
if not isinstance(data, dict) or not all(isinstance(options, dict) for options in data.values()):
raise Failure(f'cannot parse {vpnrouter.CONFIG_FILE}: it is not an object of sections')
return data
def converted(key, text):
"""The value of text for the setting key, section.option, of the type the schema gives it."""
section, _, option = key.partition('.')
if section not in vpnrouter.SCHEMA or option not in vpnrouter.SCHEMA[section]:
raise Failure(f'{key}: unknown setting; settings are section.option, such as web.port', 2)
kind = type(vpnrouter.SCHEMA[section][option])
if kind is bool:
if text not in ('true', 'false'):
raise Failure(f'{key} takes true or false', 2)
return text == 'true'
if kind is int:
try:
return int(text)
except ValueError:
raise Failure(f'{key} takes a whole number', 2)
if kind is list:
return [item.strip() for item in text.split(',') if item.strip()]
return text
def write(data):
"""Write the settings, indented, with mode 0600, replacing the file in one step."""
directory = vpnrouter.CONFIG_FILE.parent
descriptor, name = tempfile.mkstemp(dir=directory, prefix='.vpn-router.json.')
try:
with os.fdopen(descriptor, 'w', encoding='utf-8') as handle:
handle.write(json.dumps(data, indent=2) + '\n')
os.chmod(name, 0o600)
os.replace(name, vpnrouter.CONFIG_FILE)
except OSError:
os.unlink(name)
raise
def main(argv):
try:
parser = Parser(prog='vpn-config-edit', description='Show and change the router configuration.')
commands = parser.add_subparsers(dest='command', required=True, parser_class=Parser)
commands.add_parser('show', help='print the configuration as indented JSON')
setter = commands.add_parser('set', help='set a setting and write the configuration')
setter.add_argument('key', metavar='SECTION.OPTION', help='the setting, such as web.port')
setter.add_argument('value', metavar='VALUE', help='its value; a list separated by commas')
options = parser.parse_args(argv)
data = read()
if options.command == 'show':
print(json.dumps(data, indent=2))
return 0
section, _, option = options.key.partition('.')
value = converted(options.key, options.value)
data.setdefault(section, {})[option] = value
# The configuration as vpn-router-setup reads it: the defaults with the file's settings.
candidate = {name: dict(defaults) for name, defaults in vpnrouter.SCHEMA.items()}
for name, settings in data.items():
candidate.setdefault(name, {}).update(settings)
try:
vpnrouter.validate(candidate)
except vpnrouter.ConfigError as exc:
raise Failure(str(exc), 2)
write(data)
except Failure as failure:
print(f'vpn-config-edit: {failure}', file=sys.stderr)
return failure.status
except OSError as exc:
print(f'vpn-config-edit: {exc}', file=sys.stderr)
return 1
print(f'{options.key} set to {json.dumps(value)} in {vpnrouter.CONFIG_FILE}.')
print('Apply it with: systemctl restart vpn-router-setup')
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))
@@ -49,7 +49,7 @@
"port": 7081, "port": 7081,
"database": "", "database": "",
"trusted_proxies": [], "trusted_proxies": [],
"status_content": "full" "status_content": "basic"
}, },
"https": { "https": {
"enabled": true, "enabled": true,