diff --git a/ADR.md b/ADR.md index ad6d3e5..7770e6e 100644 --- a/ADR.md +++ b/ADR.md @@ -189,3 +189,29 @@ options at all. They are recognised by name as before: `psk`, and options with ` Consequences: No secret reaches the web service's state file or any answer of the API. A client cannot tell whether a secret is set. A new secret option still needs a name that matches. + +## ADR-9: The Router Manager is a separate package with a small Python server + +Status: Accepted + +Context: The router needs a web application to manage it, which may run on the router or on +another Linux system. The router's web service needs a token signed with `web.jwt_secret` for +`/config` and `/users` and sends no CORS headers, so a browser cannot call it with the secret +kept away from the page. + +Decision: `vpn-router-manager` is a Debian package of its own, built in the repository `linux-cloud-router-manager`. A React and +MUI application in the style of manage-ui is built with Vite when the package is built and +shipped as static files. A Python server, using the Ubuntu packages `python3-jwt` and `python3-requests`, serves them and +passes every `/api/` call to the router's `/`, signing a short-lived token with the +router's secret; it holds no logic of the router's own. 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. The +manager asks for no authentication yet. + +Consequences: The router needs no Node.js; the package needs only `python3` and packages of the Ubuntu archive. The secret never +reaches the browser. The application checks the shape of every answer of the router. Anyone who +reaches the manager's port manages the router, so on another system it listens on `127.0.0.1` +unless told otherwise. The router's firewall depends on whether the manager is installed, and a +changed manager port needs `vpn-router-setup` to run again. diff --git a/README.md b/README.md index 29d9983..81546a8 100644 --- a/README.md +++ b/README.md @@ -502,6 +502,14 @@ request reads the token secret from `vpn-router.json`, and the features and the before that file exists the page shows no features. `vpn-router-setup` opens `web.port` on the internal interface in the firewall. +## Router Manager + +The `vpn-router-manager` package, built in the repository `linux-cloud-router-manager`, is a 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 +the router, `vpn-router-setup` opens its port (`port` in `/etc/vpn-router-manager/manager.json`, +8090 by default) on the internal interface; restart `vpn-router-setup` after changing it. + ## Removal `apt-get remove` stops `vpn-router-docker`, which removes the Docker rules. It terminates the diff --git a/app/README.md b/app/README.md deleted file mode 100644 index e69de29..0000000 diff --git a/debian-package/debian/changelog b/debian-package/debian/changelog index dc3b1dc..f839d54 100644 --- a/debian-package/debian/changelog +++ b/debian-package/debian/changelog @@ -1,3 +1,13 @@ +vpn-router (0.5.4-1) unstable; urgency=medium + + feature: the firewall lets the Router Manager in on the internal interface. + + * While vpn-router-manager is installed, vpn-router-setup opens its port, + port in /etc/vpn-router-manager/manager.json or 8090, on the internal + interface. Purging the package removes the rule. + + -- Slawomir Koszewski Sat, 24 Oct 2026 13:00:00 +0200 + vpn-router (0.5.3-1) unstable; urgency=medium fix: the status always shows the state of slapd. diff --git a/debian-package/debian/postrm b/debian-package/debian/postrm index a2904ae..8ce563e 100755 --- a/debian-package/debian/postrm +++ b/debian-package/debian/postrm @@ -11,10 +11,10 @@ strip_ufw_blocks() { tmp="$(mktemp)" awk ' - /^# (IPSEC|WIREGUARD|OPENVPN|P2S DNS|WEB|ROUTER FORWARD|ROUTER NAT|ROUTER MSS) RULES START$/ { + /^# (IPSEC|WIREGUARD|OPENVPN|P2S DNS|WEB|MANAGER|ROUTER FORWARD|ROUTER NAT|ROUTER MSS) RULES START$/ { skip = 1; pending = 0; next } - /^# (IPSEC|WIREGUARD|OPENVPN|P2S DNS|WEB|ROUTER FORWARD|ROUTER NAT|ROUTER MSS) RULES END$/ { + /^# (IPSEC|WIREGUARD|OPENVPN|P2S DNS|WEB|MANAGER|ROUTER FORWARD|ROUTER NAT|ROUTER MSS) RULES END$/ { skip = 0; next } skip { next } diff --git a/debian-package/src/usr/lib/vpn-router/setup b/debian-package/src/usr/lib/vpn-router/setup index f5b676c..ef6932b 100755 --- a/debian-package/src/usr/lib/vpn-router/setup +++ b/debian-package/src/usr/lib/vpn-router/setup @@ -31,6 +31,11 @@ UFW_DEFAULTS = pathlib.Path('/etc/default/ufw') WG_DIR = pathlib.Path('/etc/wireguard') WEB_STATE = pathlib.Path('/run/vpn-router/web.json') +#: The Router Manager, vpn-router-manager, when it is installed, and its settings. +MANAGER = pathlib.Path('/usr/lib/vpn-router-manager/manager') +MANAGER_CONFIG = pathlib.Path('/etc/vpn-router-manager/manager.json') +MANAGER_PORT = 8090 + FILTER_ANCHOR = '# End required lines' #: Configuration options left out wherever the configuration is shown. @@ -42,6 +47,7 @@ BLOCKS = ( ('# OPENVPN RULES START', '# OPENVPN RULES END'), ('# P2S DNS RULES START', '# P2S DNS RULES END'), ('# WEB RULES START', '# WEB RULES END'), + ('# MANAGER RULES START', '# MANAGER RULES END'), ('# ROUTER FORWARD RULES START', '# ROUTER FORWARD RULES END'), ('# ROUTER NAT RULES START', '# ROUTER NAT RULES END'), ('# ROUTER MSS RULES START', '# ROUTER MSS RULES END'), @@ -129,6 +135,15 @@ def is_configured(ctx): vpnrouter.openvpn_ready(ctx))) +def manager_port(): + """The port of the Router Manager: port in its settings, or MANAGER_PORT when it is not set or not a port.""" + try: + port = json.loads(MANAGER_CONFIG.read_text(encoding='utf-8')).get('port') + except (OSError, ValueError, AttributeError): + return MANAGER_PORT + return port if isinstance(port, int) and not isinstance(port, bool) and 1 <= port <= 65535 else MANAGER_PORT + + def filter_block(ctx): lines = [ '', @@ -185,6 +200,15 @@ def filter_block(ctx): '# WEB RULES END', ] + if MANAGER.exists() and ctx['int_iface']: + port = manager_port() + lines += [ + '', + '# MANAGER RULES START', + f'-A ufw-before-input -i {ctx["int_iface"]} -p tcp --dport {port} -j ACCEPT', + '# MANAGER RULES END', + ] + if vpnrouter.routing_ready(ctx): lines += ['', '# ROUTER FORWARD RULES START'] for subnet in ctx['local_cidr_list']: diff --git a/build-and-publish.pl b/release.py similarity index 100% rename from build-and-publish.pl rename to release.py