fix: the web service listens on the internal address, the external address or both.
Release / release (push) Successful in 25s

This commit is contained in:
slawek committed 2026-10-25 11:37:43 +01:00
1 parent 581b53d447
commit 993a7a9fa9
9 files changed
+165 -89

No files matched your search

+35 -29
View File
@@ -27,28 +27,33 @@ port is reachable from the external interface only when it is in `exposed_ports`
in `DOCKER-USER` are not touched, because the package owns only its own chain. Restarting Docker in `DOCKER-USER` are not touched, because the package owns only its own chain. Restarting Docker
restores the rules. The rules cover IPv4 only. restores the rules. The rules cover IPv4 only.
## ADR-2: A small status web service, bound to the internal address ## ADR-2: A small status web service, on the interfaces web.listen chooses
Status: Accepted Status: Accepted
Context: The router has no way to see its state without a shell. A check from the protected Context: The router has no way to see its state without a shell. A check from the protected
network, by a person or by a monitor, needs a page or an endpoint. The router is an network, by a person or by a monitor, needs a page or an endpoint, and so does the platform that
internet-facing host, so what the service shows and where it listens matter. deploys the router, which reaches it on its public address. The router is an internet-facing
host, so what the service shows and where it listens matter.
Decision: The `vpn-router-web` unit runs one Python script on the standard library's Decision: The `vpn-router-web` unit runs one Python script on the standard library's
`http.server`, with the package's own modules and the Python packages of the Ubuntu archive that `http.server`, with the package's own modules and the Python packages of the Ubuntu archive that
the package depends on, and no web framework. It listens on the address of the internal interface and on the package depends on, and no web framework. It listens on `web.port`, 7081 by default, on the
`web.port`, 7081 by default, and is on unless `web.enabled` is `false` (ADR-7). `/` is for address of the internal interface, of the external interface or of both, as `web.listen` sets
people, in plain text for `curl` and `wget` and as a Tailwind CSS page for a browser, and (`internal`, `external` or `both`, the default), with a server for each address, and is on
`/status` is the same status as JSON. It shows the state of the services, the package version unless `web.enabled` is `false` (ADR-7). `/` is for people, in plain text for `curl` and `wget`
and the state of the site-to-site, road-warrior, OpenVPN and WireGuard features, and nothing from and as a Tailwind CSS page for a browser, and `/status` is the same status as JSON. It shows the
the configuration. `vpn-router-setup` opens the port on the internal interface in UFW and writes state of the services, the package version and the state of the site-to-site, road-warrior,
the features and the facts it resolves to `/run/vpn-router/web.json`. OpenVPN and WireGuard features, and nothing from the configuration. `vpn-router-setup` opens the
port on both interfaces in UFW, so the choice of `web.listen` is made by the service alone, and
writes the features and the facts it resolves to `/run/vpn-router/web.json`.
Consequences: Nothing outside the internal network can reach it, since it listens on the internal Consequences: By default the service is reachable from the internet on the external address;
address only and the firewall opens the port on the internal interface only. `/` and `/status` `web.listen` set to `internal` keeps it to the internal network. It serves plain HTTP, so the
have no authentication, which is acceptable because they are read only and show no passwords of `/auth` and the tokens cross the internet unencrypted. A change of `web.listen`
configuration. It runs as root, because `swanctl` and `wg` need it, with `NoNewPrivileges`, needs no run of setup, since the firewall does not depend on it; the package's upgrade runs
setup, which writes the rules. `/` and `/status` have no authentication, which is acceptable because they are
read only and show no configuration. It runs as root, because `swanctl` and `wg` need it, with `NoNewPrivileges`,
`ProtectSystem=strict`, `ProtectHome` and `PrivateTmp`. The Tailwind style comes from a CDN, so a `ProtectSystem=strict`, `ProtectHome` and `PrivateTmp`. The Tailwind style comes from a CDN, so a
browser on a network without internet shows the unstyled page, and `curl` is unaffected. browser on a network without internet shows the unstyled page, and `curl` is unaffected.
@@ -60,7 +65,7 @@ Context: Clients such as the Router Manager, and people with `curl`, need the co
the users and the groups of the router, which `/` and `/status` must not show. Both people and the users and the groups of the router, which `/` and `/status` must not show. Both people and
programs sign in with a user of the directory. A secret shared with the platform that deploys programs sign in with a user of the directory. A secret shared with the platform that deploys
the router would let whoever holds it, and every copy of the deployment's state, make tokens. the router would let whoever holds it, and every copy of the deployment's state, make tokens.
The service listens on plain HTTP on the internal network. The service listens on plain HTTP.
Decision: `/config`, `/users` and `/groups` need a JWT in an `Authorization: Bearer` header, Decision: `/config`, `/users` and `/groups` need a JWT in an `Authorization: Bearer` header,
HS256, verified with `hmac` and `hashlib`; no other algorithm is accepted. The key is HS256, verified with `hmac` and `hashlib`; no other algorithm is accepted. The key is
@@ -158,22 +163,23 @@ and the holders of an admin token manage users.
Status: Accepted Status: Accepted
Context: The status page is the tool to see a problem of `vpn-router-setup`, so it must run Context: The status page is the tool to see a problem of `vpn-router-setup`, so it must run
whatever state setup is in, also before setup has run and while it fails. The internal address, whatever state setup is in, also before setup has run and while it fails. The addresses,
`web.port` and `web.enabled` can change at any time. `web.listen`, `web.port` and `web.enabled` can change at any time.
Decision: The service depends on nothing to start and does not end on its own. A discovery Decision: The service depends on nothing to start and does not end on its own. A discovery
thread works out, every 30 seconds, the internal address from `vpn-router.json` as thread works out, every 30 seconds, the addresses of the interfaces `web.listen` names from
`general.mode` directs, with the discovery code in `vpnrouter_net.py`, which `vpnrouter.py` uses `vpn-router.json` as `general.mode` directs, with the discovery code in `vpnrouter_net.py`,
too, and `web.port` and `web.enabled` from the same file. It runs the HTTP server in a thread of which `vpnrouter.py` uses too, and `web.port` and `web.enabled` from the same file. It runs an
its own at that address and port, stops it and starts a new one when they change, and runs none HTTP server in a thread of its own at each address and the port, stops one and starts a new one
while the service is disabled, the address is not known or it cannot be bound. Each request when they change, and runs none while the service is disabled or no address is known; an address
that cannot be bound is tried again the next round. Each request
reads the signing key and `vpn-router.json`, and the features and resolved facts from reads the signing key and `vpn-router.json`, and the features and resolved facts from
`web.json`, which setup writes when it applies the configuration, so the service shows what setup `web.json`, which setup writes when it applies the configuration, so the service shows what setup
applied; while that file is missing the router shows no features. The unit is not applied; while that file is missing the router shows no features. The unit is not
ordered after `vpn-router-setup`, and setup does not restart it. ordered after `vpn-router-setup`, and setup does not restart it.
Consequences: The status page answers as soon as the internal address exists, whatever state Consequences: The status page answers as soon as an address exists, whatever state setup is in,
setup is in, and follows a changed address, port or `web.enabled` within 30 seconds without a and follows a changed address, `web.listen`, port or `web.enabled` within 30 seconds without a
restart. A new signing key applies to the next request. The interfaces are read every 30 restart. A new signing key applies to the next request. The interfaces are read every 30
seconds, which costs a few `ip` commands. seconds, which costs a few `ip` commands.
@@ -209,9 +215,9 @@ router's `/auth`, keeps their tokens in a server-side session in PostgreSQL behi
session's access token, refreshing it as needed; it holds no logic of the router's own and no session's access token, refreshing it as needed; it holds no logic of the router's own and no
key of it. It reads its settings from `/etc/vpn-router-manager/manager.json` at each request; on key of it. It reads its settings from `/etc/vpn-router-manager/manager.json` at each request; on
the router the settings not given come from `vpn-router.json`, and it listens on the internal the router the settings not given come from `vpn-router.json`, and it listens on the internal
address like the status web service, with the same discovery thread. `vpn-router-setup` opens address, the external address or both, as its own `listen` sets, like the status web service,
the manager's port on the internal interface while the manager is installed, so the firewall with the same discovery thread. `vpn-router-setup` opens the manager's port on the internal and
stays in one place. the external interface while the manager is installed, so the firewall stays in one place.
Consequences: The router needs no Node.js; the package needs only `python3` and packages of the Consequences: The router needs no Node.js; the package needs only `python3` and packages of the
Ubuntu archive. Tokens never reach the browser, and sessions survive a restart of the manager. Ubuntu archive. Tokens never reach the browser, and sessions survive a restart of the manager.
@@ -320,8 +326,8 @@ within 15 minutes; a successful bind clears the count; `vpn-router-user unlock`
`/auth` answers the same 401 for a wrong name, password or code and for a locked or disabled `/auth` answers the same 401 for a wrong name, password or code and for a locked or disabled
user. `/auth` and `/auth/refresh` allow 10 calls a minute per client address, counted in memory, user. `/auth` and `/auth/refresh` allow 10 calls a minute per client address, counted in memory,
and answer 429 with `Retry-After` beyond that. The client address is the peer, or the last and answer 429 with `Retry-After` beyond that. The client address is the peer, or the last
`X-Forwarded-For` entry for a peer in `web.trusted_proxies`, by default the address the service `X-Forwarded-For` entry for a peer in `web.trusted_proxies`, by default the address the call came
listens on, where a manager on the router calls from. in on, which is the address a manager on the router calls from.
Consequences: One lockout covers OpenVPN and `/auth`, kept by slapd alone. Someone who knows a Consequences: One lockout covers OpenVPN and `/auth`, kept by slapd alone. Someone who knows a
user name can lock the user out for 15 minutes at a time. The rate limit restarts with the user name can lock the user out for 15 minutes at a time. The rate limit restarts with the
+18 -14
View File
@@ -123,10 +123,11 @@ must have the type shown below; an unknown section or setting is an error.
| `openvpn` | `protocol` | string | `udp` | `udp` or `tcp` | | `openvpn` | `protocol` | string | `udp` | `udp` or `tcp` |
| `docker` | `mode` | string | `auto` | `auto` secures Docker when its service is installed, `on` requires it, `off` removes the rules | | `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 | | `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` | `enabled` | boolean | `true` | The status web service |
| `web` | `listen` | string | `both` | The interfaces the status web service listens on: `internal`, `external` or `both` |
| `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 listening address | Addresses whose `X-Forwarded-For` header names the client for the `/auth` rate limit | | `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 |
A minimal site-to-site configuration in `auto` mode: A minimal site-to-site configuration in `auto` mode:
@@ -492,8 +493,9 @@ modules shipped with the package; the configuration file accepts any module inst
## Status web service ## Status web service
The `vpn-router-web` service shows the state of the router on its internal address and The `vpn-router-web` service shows the state of the router on the addresses of the interfaces
`web.port` (7081 by default). Its pages `/` and `/status` need no authentication and show no `web.listen` names, the internal, the external or both (the default), and `web.port` (7081 by
default). Its pages `/` and `/status` need no authentication and show no
configuration: no key, identity, address or subnet. configuration: no key, identity, address or subnet.
The service first makes its signing key and the refresh tokens' database, retrying every 15 The service first makes its signing key and the refresh tokens' database, retrying every 15
@@ -585,7 +587,7 @@ removed. Changing a user's password, disabling, unlocking or removing the user r
user's refresh tokens. Five failed logins lock the account for 15 minutes (see the user user's refresh tokens. Five failed logins lock the account for 15 minutes (see the user
directory). `/auth` and `/auth/refresh` take at most 10 calls a minute from one client address, directory). `/auth` and `/auth/refresh` take at most 10 calls a minute from one client address,
and answer 429 with `Retry-After` beyond that; for a call from one of `web.trusted_proxies`, by and answer 429 with `Retry-After` beyond that; for a call from one of `web.trusted_proxies`, by
default the address the service listens on, the client address is the last entry of default the address the call came in on, the client address is the last entry of
`X-Forwarded-For`. `X-Forwarded-For`.
Refresh tokens are kept, as their SHA-256, in PostgreSQL: by default in the database and role Refresh tokens are kept, as their SHA-256, in PostgreSQL: by default in the database and role
@@ -680,13 +682,14 @@ otherwise. `DELETE /users/<name>` is refused with 400 while the user is the last
group. group.
The service starts on its own, without waiting for `vpn-router-setup`, and keeps running. Every The service starts on its own, without waiting for `vpn-router-setup`, and keeps running. Every
30 seconds it works out the internal address from `vpn-router.json` as `general.mode` directs, 30 seconds it reads `web.listen`, `web.port` and `web.enabled` from `vpn-router.json` and works
and reads `web.port` and `web.enabled`; it listens there, moves when the address or the port out the addresses of the interfaces `web.listen` names as `general.mode` directs; it listens on
changes, and stops listening while `web.enabled` is `false` or the address is not known. Each each, moves when an address or the port changes, and stops listening while `web.enabled` is
request reads the signing key and `vpn-router.json`, and the features and the facts for `false` or no address is known. A change of `web.listen` takes effect within 30 seconds, or when
`/config` from `/run/vpn-router/web.json` (readable by root only), which `vpn-router-setup` writes; the service restarts. Each request reads the signing key and `vpn-router.json`, and the features
before that file exists the page shows no features. `vpn-router-setup` opens `web.port` on the and the facts for `/config` from `/run/vpn-router/web.json` (readable by root only), which
internal interface in the firewall. `vpn-router-setup` writes; before that file exists the page shows no features.
`vpn-router-setup` opens `web.port` on the internal and the external interface in the firewall.
## Router Manager ## Router Manager
@@ -694,8 +697,9 @@ The `vpn-router-manager` package, built in the repository `linux-cloud-router-ma
application that shows the state of the router and manages its users through the status web application that shows the state of the router and manages its users through the status web
service. It can be installed on the router or on another Linux system. While it is installed on service. It can be installed on the router or on another Linux system. While it is installed on
the router, `vpn-router-setup` opens its port (`port` in `/etc/vpn-router-manager/manager.json`, the router, `vpn-router-setup` opens its port (`port` in `/etc/vpn-router-manager/manager.json`,
7080 by default, HTTP; 7443 is kept for it with TLS) on the internal interface; restart 7080 by default, HTTP; 7443 is kept for it with TLS) on the internal and the external
`vpn-router-setup` after changing it. interface; restart `vpn-router-setup` after changing it. The manager's `listen` chooses the
interfaces it listens on.
## Removal ## Removal
+13
View File
@@ -1,3 +1,16 @@
vpn-router (0.12.3-1) unstable; urgency=medium
fix: the web service listens on the internal address, the external address or both.
* web.listen in vpn-router.json sets the interfaces the web service
listens on: internal, external or both, the default. It is read
every 30 seconds, so a change takes effect without running setup,
at the latest when vpn-router-web restarts.
* The firewall opens the ports of the web service and of the Router
Manager on the internal and the external interface.
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Sun, 25 Oct 2026 13:00:00 +0200
vpn-router (0.12.2-1) unstable; urgency=medium vpn-router (0.12.2-1) unstable; urgency=medium
fix: the web service manages users also when it started before setup. fix: the web service manages users also when it started before setup.
+6 -4
View File
@@ -215,11 +215,13 @@ def filter_block(ctx):
] ]
lines += ['# P2S DNS RULES END'] lines += ['# P2S DNS RULES END']
if ctx['web_enabled'] and ctx['int_iface']: # The web service and the Router Manager choose their interfaces themselves, so their ports are open on both.
interfaces = [name for name in (ctx['int_iface'], ctx['wan_iface']) if name]
if ctx['web_enabled'] and interfaces:
lines += [ lines += [
'', '',
'# WEB RULES START', '# WEB RULES START',
f'-A ufw-before-input -i {ctx["int_iface"]} -p tcp --dport {ctx["web_port"]} -j ACCEPT', *(f'-A ufw-before-input -i {name} -p tcp --dport {ctx["web_port"]} -j ACCEPT' for name in interfaces),
'# WEB RULES END', '# WEB RULES END',
] ]
@@ -231,12 +233,12 @@ def filter_block(ctx):
'# BOOTSTRAP RULES END', '# BOOTSTRAP RULES END',
] ]
if MANAGER.exists() and ctx['int_iface']: if MANAGER.exists() and interfaces:
port = manager_port() port = manager_port()
lines += [ lines += [
'', '',
'# MANAGER RULES START', '# MANAGER RULES START',
f'-A ufw-before-input -i {ctx["int_iface"]} -p tcp --dport {port} -j ACCEPT', *(f'-A ufw-before-input -i {name} -p tcp --dport {port} -j ACCEPT' for name in interfaces),
'# MANAGER RULES END', '# MANAGER RULES END',
] ]
@@ -21,7 +21,7 @@ if str(LIB_DIR) not in sys.path:
import vpnrouter_ldap import vpnrouter_ldap
import vpnrouter_platforms import vpnrouter_platforms
from vpnrouter_net import ConfigError, detect_interfaces, interface_addresses, ip_output from vpnrouter_net import DEFAULT_LISTEN, LISTEN_CHOICES, ConfigError, detect_interfaces, interface_addresses, ip_output
CONFIG_FILE = pathlib.Path('/etc/vpn-router/vpn-router.json') CONFIG_FILE = pathlib.Path('/etc/vpn-router/vpn-router.json')
TEMPLATE_DIR = pathlib.Path('/usr/share/vpn-router/templates') TEMPLATE_DIR = pathlib.Path('/usr/share/vpn-router/templates')
@@ -54,7 +54,7 @@ SCHEMA = {
'wireguard': {'enabled': False, 'address': '', 'listen_port': 51820}, 'wireguard': {'enabled': False, 'address': '', 'listen_port': 51820},
'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, 'port': 7081, 'database': '', 'trusted_proxies': []}, 'web': {'enabled': True, 'listen': DEFAULT_LISTEN, 'port': 7081, 'database': '', 'trusted_proxies': []},
} }
DOCKER_MODES = ('auto', 'on', 'off') DOCKER_MODES = ('auto', 'on', 'off')
@@ -259,6 +259,11 @@ def validate(cfg):
if isinstance(web_port, bool) or not isinstance(web_port, int) or not 1 <= web_port <= 65535: if isinstance(web_port, bool) or not isinstance(web_port, int) or not 1 <= web_port <= 65535:
raise ConfigError(f'web.port: {web_port!r} is not a valid port') raise ConfigError(f'web.port: {web_port!r} is not a valid port')
web_listen = _get(cfg, 'web', 'listen')
if web_listen not in LISTEN_CHOICES:
raise ConfigError(
f'web.listen: {web_listen!r} is not one of {", ".join(LISTEN_CHOICES)}')
for item in _get_list(cfg, 'web', 'trusted_proxies'): for item in _get_list(cfg, 'web', 'trusted_proxies'):
_check_address(item, 'web.trusted_proxies') _check_address(item, 'web.trusted_proxies')
@@ -7,6 +7,12 @@ this module too.
import pathlib import pathlib
import subprocess import subprocess
#: The values of web.listen and of the Router Manager's listen: the interfaces a service listens on.
LISTEN_CHOICES = ('internal', 'external', 'both')
#: The interfaces a service listens on when it is not set.
DEFAULT_LISTEN = 'both'
class ConfigError(Exception): class ConfigError(Exception):
"""A setting is present but malformed.""" """A setting is present but malformed."""
@@ -110,26 +116,58 @@ def internal_address(mode, internal, int_addr):
return addresses[0] if addresses else '' return addresses[0] if addresses else ''
def web_listen(configuration, default_port): def external_address(mode, external):
"""Where the web service listens, from vpn-router.json read as configuration, as ((address, port), ''). """The router's address on the external interface, as general.mode directs, or '' when it is not known yet.
The address is the internal address as general.mode directs, the port web.port, else auto works out which interface that is; the other modes read the address of interfaces.external.
default_port. While web.enabled is false or the address is not known, the answer is (None,
the reason).
""" """
def section(name): try:
value = configuration.get(name) if mode == 'auto':
return value if isinstance(value, dict) else {} external = detect_interfaces()[0]
addresses = interface_addresses(external, 'interfaces.external')
except ConfigError:
return ''
return addresses[0] if addresses else ''
web = section('web')
def _section(configuration, name):
"""A section of a configuration, or {} when it is missing or not an object."""
value = configuration.get(name)
return value if isinstance(value, dict) else {}
def listen_addresses(configuration, choice):
"""The addresses a service listens on for choice of LISTEN_CHOICES, from vpn-router.json read as configuration.
The internal address comes first; an address not known yet is left out. A choice that is not
one of LISTEN_CHOICES is DEFAULT_LISTEN.
"""
if choice not in LISTEN_CHOICES:
choice = DEFAULT_LISTEN
general, interfaces, local = (_section(configuration, name) for name in ('general', 'interfaces', 'local'))
mode = str(general.get('mode') or 'manual')
addresses = []
if choice in ('internal', 'both'):
addresses.append(internal_address(mode, str(interfaces.get('internal') or ''), str(local.get('int_addr') or '')))
if choice in ('external', 'both'):
addresses.append(external_address(mode, str(interfaces.get('external') or '')))
return [address for address in addresses if address]
def web_listen(configuration, default_port):
"""Where the web service listens, from vpn-router.json read as configuration, as ([(address, port), ...], '').
The addresses are those of the interfaces web.listen names (listen_addresses), the port
web.port, else default_port. While web.enabled is false or no address is known, the answer
is ([], the reason).
"""
web = _section(configuration, 'web')
if web.get('enabled', True) is False: if web.get('enabled', True) is False:
return None, 'the web service is disabled in the configuration' return [], 'the web service is disabled in the configuration'
port = web.get('port', default_port) port = web.get('port', default_port)
if isinstance(port, bool) or not isinstance(port, int) or not 1 <= port <= 65535: if isinstance(port, bool) or not isinstance(port, int) or not 1 <= port <= 65535:
port = default_port port = default_port
general, interfaces, local = section('general'), section('interfaces'), section('local') addresses = listen_addresses(configuration, web.get('listen', DEFAULT_LISTEN))
address = internal_address(str(general.get('mode') or 'manual'), str(interfaces.get('internal') or ''), if not addresses:
str(local.get('int_addr') or '')) return [], 'the addresses to listen on are not known yet'
if not address: return [(address, port) for address in addresses], ''
return None, 'the internal address is not known yet'
return (address, port), ''
@@ -10,31 +10,37 @@ from http.server import ThreadingHTTPServer
def serve(wanted, handler, log, interval): def serve(wanted, handler, log, interval):
"""Work out where to listen every interval seconds with wanted, and run an HTTP server of handler there. """Work out where to listen every interval seconds with wanted, and run an HTTP server of handler at each place.
wanted answers ((address, port), '') or (None, the reason not to listen). When the address or wanted answers ([(address, port), ...], '') or ([], the reason not to listen). A server whose
the port changes, the server is stopped and a new one listens at the new place; while there is place is no longer wanted is stopped, and a new one listens at each new place; a place that
no place or it cannot be bound, no server runs and the next round tries again. Every new cannot be bound is tried again the next round. Every new message is logged with log. Does
reason is logged with log. Does not return. not return.
""" """
server, thread, listening, reported = None, None, None, None servers, reported = {}, None
while True: while True:
target, reason = wanted() targets, reason = wanted()
if target != listening and server is not None: for target in [target for target in servers if target not in targets]:
server, thread = servers.pop(target)
server.shutdown() server.shutdown()
server.server_close() server.server_close()
thread.join() thread.join()
server, listening = None, None messages = [reason] if reason else []
if target is not None and server is None: for target in targets:
if target in servers:
continue
try: try:
server = ThreadingHTTPServer(target, handler) server = ThreadingHTTPServer(target, handler)
except OSError as exc: except OSError as exc:
reason = f'cannot listen on {target[0]}:{target[1]}: {exc}' messages.append(f'cannot listen on {target[0]}:{target[1]}: {exc}')
else: continue
thread = threading.Thread(target=server.serve_forever, name='server', daemon=True) thread = threading.Thread(target=server.serve_forever, name=f'server {target[0]}', daemon=True)
thread.start() thread.start()
listening, reason = target, f'serving on http://{target[0]}:{target[1]}' servers[target] = (server, thread)
if reason and reason != reported: if servers:
log(reason) messages.append('serving on ' + ', '.join(f'http://{address}:{port}' for address, port in servers))
reported = reason message = '; '.join(messages)
if message and message != reported:
log(message)
reported = message
time.sleep(interval) time.sleep(interval)
+7 -6
View File
@@ -1,5 +1,5 @@
#!/usr/bin/python3 #!/usr/bin/python3
"""Serve the status of the router on its internal address. """Serve the status of the router on its internal address, its external address or both, as web.listen sets.
Run by vpn-router-web.service. Users are managed with vpnrouter_users, the module that Run by vpn-router-web.service. Users are managed with vpnrouter_users, the module that
vpn-router-user uses too. vpn-router-user uses too.
@@ -27,7 +27,7 @@ token of vpn-router-token has the admin claim and names no user; it may call eve
add or take out its members, and remove one of its members. These paths and /auth are not add or take out its members, and remove one of its members. These paths and /auth are not
served (404) while the key does not exist. /auth is limited to AUTH_LIMIT calls per client served (404) while the key does not exist. /auth is limited to AUTH_LIMIT calls per client
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 service listens 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: whether the services run, the package version and the / and /status show only the status: whether the services run, the package version and the
installed version of vpn-router-manager (manager_version, null when it is not installed), whether installed version of vpn-router-manager (manager_version, null when it is not installed), whether
@@ -40,10 +40,11 @@ logging each failure. Then it listens. Its unit starts it only once wait-for-lib
in the linker cache, where psycopg looks it up. in the linker cache, where psycopg looks it up.
The service needs nothing else to start and does not end on its own. Every DISCOVERY_SECONDS it The service needs nothing else to start and does not end on its own. Every DISCOVERY_SECONDS it
works out the internal address from /etc/vpn-router/vpn-router.json as general.mode directs, works out from /etc/vpn-router/vpn-router.json the addresses of the interfaces web.listen names,
with web.port and web.enabled (vpnrouter_net.web_listen), and runs the server there internal, external or both, as general.mode directs, with web.port and web.enabled
(vpnrouter_serve.serve): it starts it, moves it when the address or the port changes, and stops (vpnrouter_net.web_listen), and runs a server on each (vpnrouter_serve.serve): it starts them,
it while the service is disabled or the address is not known. Every request reads what it needs when it is made: the signing moves them when an address or the port changes, and stops them while the service is disabled or
no address is known. Every request reads what it needs when it is made: the signing
key, the configuration, and the features of the router and the facts shown by /config from key, the configuration, and the features of the router and the facts shown by /config from
/run/vpn-router/web.json, which vpn-router-setup writes. Before setup has written that file /run/vpn-router/web.json, which vpn-router-setup writes. Before setup has written that file
the router has no features, and /config shows no configuration or interfaces. the router has no features, and /config shows no configuration or interfaces.
@@ -45,6 +45,7 @@
}, },
"web": { "web": {
"enabled": true, "enabled": true,
"listen": "both",
"port": 7081, "port": 7081,
"database": "", "database": "",
"trusted_proxies": [] "trusted_proxies": []