fix: the web service listens on the internal address, the external address or both.
Release / release (push) Successful in 25s
Release / release (push) Successful in 25s
This commit is contained in:
1 parent
581b53d447
commit
993a7a9fa9
9 files changed
+165
-89
No files matched your search
@@ -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
|
||||
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
|
||||
|
||||
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
|
||||
internet-facing host, so what the service shows and where it listens matter.
|
||||
network, by a person or by a monitor, needs a page or an endpoint, and so does the platform that
|
||||
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
|
||||
`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
|
||||
`web.port`, 7081 by default, and is on 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. It shows the state of the services, the package version
|
||||
and the state of the site-to-site, road-warrior, OpenVPN and WireGuard features, and nothing from
|
||||
the configuration. `vpn-router-setup` opens the port on the internal interface in UFW and writes
|
||||
the features and the facts it resolves to `/run/vpn-router/web.json`.
|
||||
the package depends on, and no web framework. It listens on `web.port`, 7081 by default, on the
|
||||
address of the internal interface, of the external interface or of both, as `web.listen` sets
|
||||
(`internal`, `external` or `both`, the default), with a server for each address, and is on
|
||||
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. It shows the
|
||||
state of the services, the package version and the state of the site-to-site, road-warrior,
|
||||
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
|
||||
address only and the firewall opens the port on the internal interface only. `/` 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`,
|
||||
Consequences: By default the service is reachable from the internet on the external address;
|
||||
`web.listen` set to `internal` keeps it to the internal network. It serves plain HTTP, so the
|
||||
passwords of `/auth` and the tokens cross the internet unencrypted. A change of `web.listen`
|
||||
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
|
||||
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
|
||||
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 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,
|
||||
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
|
||||
|
||||
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,
|
||||
`web.port` and `web.enabled` can change at any time.
|
||||
whatever state setup is in, also before setup has run and while it fails. The addresses,
|
||||
`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
|
||||
thread works out, every 30 seconds, the internal address from `vpn-router.json` as
|
||||
`general.mode` directs, with the discovery code in `vpnrouter_net.py`, which `vpnrouter.py` uses
|
||||
too, and `web.port` and `web.enabled` from the same file. It runs the HTTP server in a thread of
|
||||
its own at that address and port, stops it and starts a new one when they change, and runs none
|
||||
while the service is disabled, the address is not known or it cannot be bound. Each request
|
||||
thread works out, every 30 seconds, the addresses of the interfaces `web.listen` names from
|
||||
`vpn-router.json` as `general.mode` directs, with the discovery code in `vpnrouter_net.py`,
|
||||
which `vpnrouter.py` uses too, and `web.port` and `web.enabled` from the same file. It runs an
|
||||
HTTP server in a thread of its own at each address and the port, stops one and starts a new one
|
||||
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
|
||||
`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
|
||||
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
|
||||
setup is in, and follows a changed address, port or `web.enabled` within 30 seconds without a
|
||||
Consequences: The status page answers as soon as an address exists, whatever state setup is in,
|
||||
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
|
||||
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
|
||||
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
|
||||
address like the status web service, with the same discovery thread. `vpn-router-setup` opens
|
||||
the manager's port on the internal interface while the manager is installed, so the firewall
|
||||
stays in one place.
|
||||
address, the external address or both, as its own `listen` sets, like the status web service,
|
||||
with the same discovery thread. `vpn-router-setup` opens the manager's port on the internal and
|
||||
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
|
||||
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
|
||||
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
|
||||
`X-Forwarded-For` entry for a peer in `web.trusted_proxies`, by default the address the service
|
||||
listens on, where a manager on the router calls from.
|
||||
`X-Forwarded-For` entry for a peer in `web.trusted_proxies`, by default the address the call came
|
||||
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
|
||||
user name can lock the user out for 15 minutes at a time. The rate limit restarts with the
|
||||
|
||||
@@ -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` |
|
||||
| `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 |
|
||||
| `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` | `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:
|
||||
|
||||
@@ -492,8 +493,9 @@ modules shipped with the package; the configuration file accepts any module inst
|
||||
|
||||
## Status web service
|
||||
|
||||
The `vpn-router-web` service shows the state of the router on its internal address and
|
||||
`web.port` (7081 by default). Its pages `/` and `/status` need no authentication and show no
|
||||
The `vpn-router-web` service shows the state of the router on the addresses of the interfaces
|
||||
`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.
|
||||
|
||||
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
|
||||
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
|
||||
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`.
|
||||
|
||||
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.
|
||||
|
||||
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,
|
||||
and reads `web.port` and `web.enabled`; it listens there, moves when the address or the port
|
||||
changes, and stops listening while `web.enabled` is `false` or the address is not known. Each
|
||||
request reads the signing key and `vpn-router.json`, and the features and the facts for
|
||||
`/config` from `/run/vpn-router/web.json` (readable by root only), which `vpn-router-setup` writes;
|
||||
before that file exists the page shows no features. `vpn-router-setup` opens `web.port` on the
|
||||
internal interface in the firewall.
|
||||
30 seconds it reads `web.listen`, `web.port` and `web.enabled` from `vpn-router.json` and works
|
||||
out the addresses of the interfaces `web.listen` names as `general.mode` directs; it listens on
|
||||
each, moves when an address or the port changes, and stops listening while `web.enabled` is
|
||||
`false` or no address is known. A change of `web.listen` takes effect within 30 seconds, or when
|
||||
the service restarts. Each request reads the signing key and `vpn-router.json`, and the features
|
||||
and the facts for `/config` from `/run/vpn-router/web.json` (readable by root only), which
|
||||
`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
|
||||
|
||||
@@ -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
|
||||
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`,
|
||||
7080 by default, HTTP; 7443 is kept for it with TLS) on the internal interface; restart
|
||||
`vpn-router-setup` after changing it.
|
||||
7080 by default, HTTP; 7443 is kept for it with TLS) on the internal and the external
|
||||
interface; restart `vpn-router-setup` after changing it. The manager's `listen` chooses the
|
||||
interfaces it listens on.
|
||||
|
||||
## Removal
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
fix: the web service manages users also when it started before setup.
|
||||
|
||||
@@ -215,11 +215,13 @@ def filter_block(ctx):
|
||||
]
|
||||
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 += [
|
||||
'',
|
||||
'# 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',
|
||||
]
|
||||
|
||||
@@ -231,12 +233,12 @@ def filter_block(ctx):
|
||||
'# BOOTSTRAP RULES END',
|
||||
]
|
||||
|
||||
if MANAGER.exists() and ctx['int_iface']:
|
||||
if MANAGER.exists() and interfaces:
|
||||
port = manager_port()
|
||||
lines += [
|
||||
'',
|
||||
'# 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',
|
||||
]
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ if str(LIB_DIR) not in sys.path:
|
||||
|
||||
import vpnrouter_ldap
|
||||
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')
|
||||
TEMPLATE_DIR = pathlib.Path('/usr/share/vpn-router/templates')
|
||||
@@ -54,7 +54,7 @@ SCHEMA = {
|
||||
'wireguard': {'enabled': False, 'address': '', 'listen_port': 51820},
|
||||
'openvpn': {'enabled': False, 'address_pool': '', 'port': 1194, 'protocol': 'udp'},
|
||||
'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')
|
||||
@@ -259,6 +259,11 @@ def validate(cfg):
|
||||
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')
|
||||
|
||||
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'):
|
||||
_check_address(item, 'web.trusted_proxies')
|
||||
|
||||
|
||||
@@ -7,6 +7,12 @@ this module too.
|
||||
import pathlib
|
||||
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):
|
||||
"""A setting is present but malformed."""
|
||||
@@ -110,26 +116,58 @@ def internal_address(mode, internal, int_addr):
|
||||
return addresses[0] if addresses else ''
|
||||
|
||||
|
||||
def web_listen(configuration, default_port):
|
||||
"""Where the web service listens, from vpn-router.json read as configuration, as ((address, port), '').
|
||||
def external_address(mode, external):
|
||||
"""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
|
||||
default_port. While web.enabled is false or the address is not known, the answer is (None,
|
||||
the reason).
|
||||
auto works out which interface that is; the other modes read the address of interfaces.external.
|
||||
"""
|
||||
def section(name):
|
||||
value = configuration.get(name)
|
||||
return value if isinstance(value, dict) else {}
|
||||
try:
|
||||
if mode == 'auto':
|
||||
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:
|
||||
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)
|
||||
if isinstance(port, bool) or not isinstance(port, int) or not 1 <= port <= 65535:
|
||||
port = default_port
|
||||
general, interfaces, local = section('general'), section('interfaces'), section('local')
|
||||
address = internal_address(str(general.get('mode') or 'manual'), str(interfaces.get('internal') or ''),
|
||||
str(local.get('int_addr') or ''))
|
||||
if not address:
|
||||
return None, 'the internal address is not known yet'
|
||||
return (address, port), ''
|
||||
addresses = listen_addresses(configuration, web.get('listen', DEFAULT_LISTEN))
|
||||
if not addresses:
|
||||
return [], 'the addresses to listen on are not known yet'
|
||||
return [(address, port) for address in addresses], ''
|
||||
@@ -10,31 +10,37 @@ from http.server import ThreadingHTTPServer
|
||||
|
||||
|
||||
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
|
||||
the port changes, the server is stopped and a new one listens at the new place; while there is
|
||||
no place or it cannot be bound, no server runs and the next round tries again. Every new
|
||||
reason is logged with log. Does not return.
|
||||
wanted answers ([(address, port), ...], '') or ([], the reason not to listen). A server whose
|
||||
place is no longer wanted is stopped, and a new one listens at each new place; a place that
|
||||
cannot be bound is tried again the next round. Every new message is logged with log. Does
|
||||
not return.
|
||||
"""
|
||||
server, thread, listening, reported = None, None, None, None
|
||||
servers, reported = {}, None
|
||||
while True:
|
||||
target, reason = wanted()
|
||||
if target != listening and server is not None:
|
||||
targets, reason = wanted()
|
||||
for target in [target for target in servers if target not in targets]:
|
||||
server, thread = servers.pop(target)
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
thread.join()
|
||||
server, listening = None, None
|
||||
if target is not None and server is None:
|
||||
messages = [reason] if reason else []
|
||||
for target in targets:
|
||||
if target in servers:
|
||||
continue
|
||||
try:
|
||||
server = ThreadingHTTPServer(target, handler)
|
||||
except OSError as exc:
|
||||
reason = f'cannot listen on {target[0]}:{target[1]}: {exc}'
|
||||
else:
|
||||
thread = threading.Thread(target=server.serve_forever, name='server', daemon=True)
|
||||
thread.start()
|
||||
listening, reason = target, f'serving on http://{target[0]}:{target[1]}'
|
||||
if reason and reason != reported:
|
||||
log(reason)
|
||||
reported = reason
|
||||
messages.append(f'cannot listen on {target[0]}:{target[1]}: {exc}')
|
||||
continue
|
||||
thread = threading.Thread(target=server.serve_forever, name=f'server {target[0]}', daemon=True)
|
||||
thread.start()
|
||||
servers[target] = (server, thread)
|
||||
if servers:
|
||||
messages.append('serving on ' + ', '.join(f'http://{address}:{port}' for address, port in servers))
|
||||
message = '; '.join(messages)
|
||||
if message and message != reported:
|
||||
log(message)
|
||||
reported = message
|
||||
time.sleep(interval)
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/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
|
||||
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
|
||||
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
|
||||
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
|
||||
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.
|
||||
|
||||
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,
|
||||
with web.port and web.enabled (vpnrouter_net.web_listen), and runs the server there
|
||||
(vpnrouter_serve.serve): it starts it, moves it when the address or the port changes, and stops
|
||||
it while the service is disabled or the address is not known. Every request reads what it needs when it is made: the signing
|
||||
works out from /etc/vpn-router/vpn-router.json the addresses of the interfaces web.listen names,
|
||||
internal, external or both, as general.mode directs, with web.port and web.enabled
|
||||
(vpnrouter_net.web_listen), and runs a server on each (vpnrouter_serve.serve): it starts them,
|
||||
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
|
||||
/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.
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
},
|
||||
"web": {
|
||||
"enabled": true,
|
||||
"listen": "both",
|
||||
"port": 7081,
|
||||
"database": "",
|
||||
"trusted_proxies": []
|
||||
|
||||
Reference in new issue
Block a user