From 9255d56276ac1775527d4bbd7479898e0b5cabb5 Mon Sep 17 00:00:00 2001 From: Slawomir Koszewski Date: Mon, 26 Oct 2026 00:57:22 +0100 Subject: [PATCH] feature: /remote-access of the web service shows and changes the settings of IKEv2 road-warrior, OpenVPN and WireGuard access and whether each runs, for the Router Manager's Remote access page. --- README.md | 19 ++++++- debian-package/debian/changelog | 12 ++++ .../src/usr/lib/vpn-router/vpnrouter.py | 3 + debian-package/src/usr/lib/vpn-router/web | 55 ++++++++++++++++++- 4 files changed, 86 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3808ff5..e6f513a 100644 --- a/README.md +++ b/README.md @@ -901,7 +901,7 @@ curl http://10.0.0.4:7081/status ### Tokens and signing in -`/config`, `/users`, `/service-accounts`, `/groups`, `/site-to-site`, `/shell` and `/account` need a JWT in an `Authorization: Bearer` header, signed with +`/config`, `/users`, `/service-accounts`, `/groups`, `/site-to-site`, `/backup`, `/remote-access`, `/ca`, `/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). 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 @@ -1142,6 +1142,23 @@ The answer of `GET /backup` holds: A change is made, checked and applied as for `/site-to-site`. +### The /remote-access API + +`/remote-access` shows and changes the remote access systems, IKEv2 road-warrior (`p2s`), +OpenVPN (`openvpn`) and WireGuard (`wireguard`), with the same token as `/users`. + +| Request | Body | Answer | +| --- | --- | --- | +| `GET /remote-access` | | `{"p2s", "openvpn", "wireguard", "running", "local_fqdn"}` | +| `PUT /remote-access/` | any of the settings of the section `p2s`, `openvpn` or `wireguard` | the section | + +`running` tells for each system whether setup runs it. IKEv2 and OpenVPN run once enabled with an +address pool, `wan.local_fqdn` set and the router's VPN server certificate issued by the P2S CA; +WireGuard once enabled with an address. A system enabled without its address pool or address is +refused. A change is made, checked and applied as for `/site-to-site`. The router's firewall +follows the settings; a cloud firewall in front of the router, such as an Azure network security +group, must allow the OpenVPN and WireGuard ports itself. + ### The /ca API `/ca` manages the PKI (see Certificates) with the same token as `/users`, with the module diff --git a/debian-package/debian/changelog b/debian-package/debian/changelog index 2be2659..49e208b 100644 --- a/debian-package/debian/changelog +++ b/debian-package/debian/changelog @@ -1,3 +1,15 @@ +vpn-router (0.24.0-1) unstable; urgency=medium + + feature: /remote-access of the web service shows and changes the settings of IKEv2 road-warrior, OpenVPN and WireGuard access and whether each runs, for the Router Manager's Remote access page. + + * GET /remote-access answers the p2s, openvpn and wireguard sections, + which of them setup runs, and wan.local_fqdn; PUT + /remote-access/ changes one section and restarts setup. + * p2s.enabled and openvpn.enabled need their address_pool, and + wireguard.enabled its address. + + -- Slawomir Koszewski Mon, 02 Nov 2026 23:00:00 +0100 + vpn-router (0.23.2-1) unstable; urgency=medium fix: the shell of a user's own POSIX account opens on the first setup that sets up console login, and the journal records each refused shell with the reason. diff --git a/debian-package/src/usr/lib/vpn-router/vpnrouter.py b/debian-package/src/usr/lib/vpn-router/vpnrouter.py index 3341ad4..1f59543 100644 --- a/debian-package/src/usr/lib/vpn-router/vpnrouter.py +++ b/debian-package/src/usr/lib/vpn-router/vpnrouter.py @@ -435,6 +435,9 @@ def validate(cfg): raise ValueError except ValueError: raise ConfigError(f'openvpn.address_pool: {openvpn_pool!r} is not an IPv4 network') + for system, option in (('p2s', 'address_pool'), ('openvpn', 'address_pool'), ('wireguard', 'address')): + if _get(cfg, system, 'enabled') and not _get(cfg, system, option): + raise ConfigError(f'{system}.enabled needs {system}.{option}') _check_address(_get(cfg, 'local', 'int_addr'), 'local.int_addr') _check_address(_get(cfg, 'local', 'int_gateway_ip'), 'local.int_gateway_ip') diff --git a/debian-package/src/usr/lib/vpn-router/web b/debian-package/src/usr/lib/vpn-router/web index 01591bd..cd7a01f 100644 --- a/debian-package/src/usr/lib/vpn-router/web +++ b/debian-package/src/usr/lib/vpn-router/web @@ -28,6 +28,9 @@ Pages: applied by vpn-router-setup (see site_to_site()) /backup the backup settings, the last run and the local archives: show, change and run now (see backup()) + /remote-access + the settings of IKEv2 road-warrior, OpenVPN and WireGuard access and whether each + runs: show and change, applied by vpn-router-setup (see remote_access()) /ca the PKI: its settings, the root and issuing CAs, the certificates issued; change the settings, issue and revoke certificates, and take the certificate of an issuing CA and a root CRL signed offline (see ca()) @@ -46,7 +49,7 @@ Pages: the databases of each PostgreSQL cluster on the router with their sizes, as JSON (see postgresql_databases()); /config has the clusters' settings -/status/full, /config, /metrics/history, /postgresql, /users, /service-accounts, /groups, /site-to-site, /backup, /ca, /shell and /account need an HS256 JWT in an Authorization: Bearer header signed +/status/full, /config, /metrics/history, /postgresql, /users, /service-accounts, /groups, /site-to-site, /backup, /remote-access, /ca, /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; /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 @@ -150,6 +153,9 @@ BODY_LIMIT = 65536 #: The settings of a site_to_site connection that /site-to-site adds and changes. CONNECTION_SETTINGS = ('addrs', 'id', 'cidrs', 'local_cidrs', 'local_id_mode', 'auth', 'psk', 'remote_ca') +#: The sections of the remote access systems /remote-access changes: IKEv2 road-warrior, OpenVPN and WireGuard. +REMOTE_ACCESS = ('p2s', 'openvpn', 'wireguard') + #: The HTTP status for each status of a vpnrouter_users.Failure; any other is 500. USER_STATUS = {2: 400, 3: 404, 4: 409, 5: 401} @@ -814,6 +820,48 @@ def backup(method, path, body): return 200, {'settings': backup_settings(cfg['backup'])} +def remote_access(state, method, path, body): + """Answer a /remote-access request: the HTTP status and the JSON document. + + GET /remote-access {"p2s", "openvpn", "wireguard", "running", "local_fqdn"}: the settings of the three + remote access systems (REMOTE_ACCESS), whether setup runs each (running: {p2s, + openvpn, wireguard}), and wan.local_fqdn, which IKEv2 and OpenVPN need with the + router's VPN server certificate + PUT /remote-access/ change any of the settings of the system's section; answers the section + + A change is made as /site-to-site makes one, and 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'} + body = body or {} + parts = path.strip('/').split('/') + try: + if (method, len(parts)) == ('GET', 1): + cfg = vpnrouter.load_config() + return 200, {**{system: cfg[system] for system in REMOTE_ACCESS}, + 'running': {system: bool(state.get(system)) for system in REMOTE_ACCESS}, + 'local_fqdn': cfg['wan']['local_fqdn']} + if method != 'PUT' or len(parts) != 2 or parts[1] not in REMOTE_ACCESS: + return 404, {'error': f'no route {method} {path}'} + system = parts[1] + settings = {option: body[option] for option in vpnrouter.SCHEMA[system] if option in body} + with vpnrouter.config_lock(): + data = vpnrouter.read_settings() + current = data.get(system) + data[system] = {**(current if isinstance(current, dict) else {}), **settings} + cfg = vpnrouter.with_defaults(data) + vpnrouter.validate(cfg) + vpnrouter.save_config(data) + except vpnrouter.ConfigError as exc: + return 400, {'error': str(exc)} + except OSError as exc: + return 500, {'error': f'the configuration cannot be written: {exc}'} + except SystemExit as exc: + return 500, {'error': str(exc.code)} + run('systemctl', 'restart', '--no-block', 'vpn-router-setup.service') + return 200, cfg[system] + + def ca(method, path, body): """Answer a /ca request with vpnrouter_pki: the HTTP status and the JSON document. @@ -1319,7 +1367,7 @@ class Handler(BaseHTTPRequestHandler): if self.command not in ('GET', 'HEAD'): self.close_connection = True directory = next((name for name in ('users', 'service-accounts', 'groups', 'account', 'site-to-site', 'backup', - 'ca', 'shell') + 'ca', 'shell', 'remote-access') if path == f'/{name}' or path.startswith(f'/{name}/')), None) protected = path in ('/status/full', '/config', '/metrics/history', '/postgresql') or directory is not None signing = path in ('/auth', '/auth/refresh', '/auth/logout') @@ -1371,6 +1419,9 @@ class Handler(BaseHTTPRequestHandler): elif directory == 'site-to-site': code, document = site_to_site(self.command, path, payload) body, kind = json.dumps(document, indent=2).encode() + b'\n', 'application/json' + elif directory == 'remote-access': + code, document = remote_access(state, self.command, path, payload) + body, kind = json.dumps(document, indent=2).encode() + b'\n', 'application/json' elif directory == 'backup': code, document = backup(self.command, path, payload) body, kind = json.dumps(document, indent=2).encode() + b'\n', 'application/json'