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
|
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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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": []
|
||||||
|
|||||||
Reference in new issue
Block a user