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.
Release / release (push) Successful in 14s

This commit is contained in:
slawek committed 2026-10-26 00:57:22 +01:00
1 parent 07dbe91d0c
commit 9255d56276
4 files changed
+86 -3

No files matched your search

+18 -1
View File
@@ -901,7 +901,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`, `/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). 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
@@ -1142,6 +1142,23 @@ 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 /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/<system>` | 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 ### The /ca API
`/ca` manages the PKI (see Certificates) with the same token as `/users`, with the module `/ca` manages the PKI (see Certificates) with the same token as `/users`, with the module
+12
View File
@@ -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/<system> changes one section and restarts setup.
* p2s.enabled and openvpn.enabled need their address_pool, and
wireguard.enabled its address.
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Mon, 02 Nov 2026 23:00:00 +0100
vpn-router (0.23.2-1) unstable; urgency=medium 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. 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.
@@ -435,6 +435,9 @@ def validate(cfg):
raise ValueError raise ValueError
except ValueError: except ValueError:
raise ConfigError(f'openvpn.address_pool: {openvpn_pool!r} is not an IPv4 network') 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_addr'), 'local.int_addr')
_check_address(_get(cfg, 'local', 'int_gateway_ip'), 'local.int_gateway_ip') _check_address(_get(cfg, 'local', 'int_gateway_ip'), 'local.int_gateway_ip')
+53 -2
View File
@@ -28,6 +28,9 @@ Pages:
applied by vpn-router-setup (see site_to_site()) applied by vpn-router-setup (see site_to_site())
/backup the backup settings, the last run and the local archives: show, change and run /backup the backup settings, the last run and the local archives: show, change and run
now (see backup()) 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 /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 settings, issue and revoke certificates, and take the certificate of an issuing CA
and a root CRL signed offline (see 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 the databases of each PostgreSQL cluster on the router with their sizes, as JSON (see
postgresql_databases()); /config has the clusters' settings 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; 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
@@ -150,6 +153,9 @@ BODY_LIMIT = 65536
#: The settings of a site_to_site connection that /site-to-site adds and changes. #: 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') 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. #: 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} 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'])} 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/<system> 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): def ca(method, path, body):
"""Answer a /ca request with vpnrouter_pki: the HTTP status and the JSON document. """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'): if self.command not in ('GET', 'HEAD'):
self.close_connection = True self.close_connection = True
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') 'ca', 'shell', 'remote-access')
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', '/postgresql') or directory is not None
signing = path in ('/auth', '/auth/refresh', '/auth/logout') signing = path in ('/auth', '/auth/refresh', '/auth/logout')
@@ -1371,6 +1419,9 @@ class Handler(BaseHTTPRequestHandler):
elif directory == 'site-to-site': elif directory == 'site-to-site':
code, document = site_to_site(self.command, path, payload) code, document = site_to_site(self.command, path, payload)
body, kind = json.dumps(document, indent=2).encode() + b'\n', 'application/json' 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': elif directory == 'backup':
code, document = backup(self.command, path, payload) code, document = backup(self.command, path, payload)
body, kind = json.dumps(document, indent=2).encode() + b'\n', 'application/json' body, kind = json.dumps(document, indent=2).encode() + b'\n', 'application/json'