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
@@ -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