Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
609921348b | ||
|
|
90e7c79840 | ||
|
|
a6b25b8bb4 |
No files matched your search
@@ -217,7 +217,9 @@ external interface, in this order:
|
|||||||
Traffic from every other interface, including the internal one and the Docker bridges, is
|
Traffic from every other interface, including the internal one and the Docker bridges, is
|
||||||
not affected. The rules cover IPv4 only.
|
not affected. The rules cover IPv4 only.
|
||||||
|
|
||||||
The `vpn-router-docker` service applies the rules after `docker.service` starts and removes
|
`vpn-router-setup` starts after `docker.service`, so Docker is up when it looks for it; the
|
||||||
|
ordering has no effect on a host without Docker. The `vpn-router-docker` service applies the
|
||||||
|
rules after `docker.service` starts and removes
|
||||||
them when Docker stops, so restarting Docker restores them. `systemctl restart
|
them when Docker stops, so restarting Docker restores them. `systemctl restart
|
||||||
vpn-router-setup` applies a changed configuration while Docker runs. With `docker.mode` set to
|
vpn-router-setup` applies a changed configuration while Docker runs. With `docker.mode` set to
|
||||||
`on`, the service fails when Docker is not installed or its daemon is not running; with
|
`on`, the service fails when Docker is not installed or its daemon is not running; with
|
||||||
@@ -356,8 +358,8 @@ curl http://10.0.0.4:8080/status
|
|||||||
| Key | Content |
|
| Key | Content |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `configuration` | `vpn-router.json` with the defaults applied and the secrets masked (`psk`, `jwt_secret`, and any option named like a password, token or key) |
|
| `configuration` | `vpn-router.json` with the defaults applied and the secrets masked (`psk`, `jwt_secret`, and any option named like a password, token or key) |
|
||||||
| `network` | The external and internal interface, their addresses, the internal gateway, the local and remote IKE identities and subnets, and the IPv4 routes |
|
| `network` | The external and internal interface, their addresses, the internal gateway, the local and remote IKE identities and subnets, and the IPv4 routes as `ip -j -4 route` prints them |
|
||||||
| `ipsec` | The output of `swanctl --list-conns` and `swanctl --list-sas`, as lists of lines |
|
| `ipsec` | `connections` and `sas`: one object per connection and per IKE SA, read from `swanctl --list-conns --pretty` and `swanctl --list-sas --pretty`, with the connection's name under `name` and swanctl's keys and string values |
|
||||||
| `wireguard` | The public key and port of `wg0`, and each peer's public key, endpoint, allowed IPs, handshake age and traffic; no private or preshared key |
|
| `wireguard` | The public key and port of `wg0`, and each peer's public key, endpoint, allowed IPs, handshake age and traffic; no private or preshared key |
|
||||||
| `docker` | `docker.exposed_ports` and the rules of the `VPN-ROUTER-DOCKER` chain |
|
| `docker` | `docker.exposed_ports` and the rules of the `VPN-ROUTER-DOCKER` chain |
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,35 @@
|
|||||||
|
vpn-router (0.4.3-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* swanctl output with more closing than opening lines is read as not
|
||||||
|
available instead of failing the request.
|
||||||
|
|
||||||
|
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Fri, 23 Oct 2026 22:00:00 +0200
|
||||||
|
|
||||||
|
vpn-router (0.4.2-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* /config gives the IPsec connections and SAs as objects, read from
|
||||||
|
swanctl --pretty, and the routes as objects from ip -j, instead of
|
||||||
|
lines of text. The site-to-site and road-warrior state of /status is
|
||||||
|
counted from the same swanctl objects.
|
||||||
|
|
||||||
|
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Fri, 23 Oct 2026 21:30:00 +0200
|
||||||
|
|
||||||
|
vpn-router (0.4.1-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Fix the start of vpn-router-setup hanging on a host that runs Docker.
|
||||||
|
vpn-router-setup restarted vpn-router-docker and waited for it, but that
|
||||||
|
unit was ordered after vpn-router-setup, so each waited for the other,
|
||||||
|
the installation of the package never finished and vpn-router-web was
|
||||||
|
never enabled or started. vpn-router-docker is no longer ordered after
|
||||||
|
vpn-router-setup, so vpn-router-setup still waits for it and the Docker
|
||||||
|
rules are in place when it ends. The restart of vpn-router-web is queued
|
||||||
|
without waiting.
|
||||||
|
* Start vpn-router-setup after docker.service, so that Docker is up when
|
||||||
|
the setup looks for it and does not start in parallel with it. The
|
||||||
|
ordering applies only when Docker is installed.
|
||||||
|
|
||||||
|
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Fri, 23 Oct 2026 20:10:00 +0200
|
||||||
|
|
||||||
vpn-router (0.4.0-1) unstable; urgency=medium
|
vpn-router (0.4.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
* Add the vpn-router-web service, a small status web service that uses only
|
* Add the vpn-router-web service, a small status web service that uses only
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=VPN Router rules for Docker published ports
|
Description=VPN Router rules for Docker published ports
|
||||||
Documentation=file:/usr/share/doc/vpn-router/README.md
|
Documentation=file:/usr/share/doc/vpn-router/README.md
|
||||||
After=docker.service vpn-router-setup.service
|
After=docker.service
|
||||||
PartOf=docker.service
|
PartOf=docker.service
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
Description=VPN Router Setup
|
Description=VPN Router Setup
|
||||||
Documentation=file:/usr/share/doc/vpn-router/README.md
|
Documentation=file:/usr/share/doc/vpn-router/README.md
|
||||||
Wants=network-online.target
|
Wants=network-online.target
|
||||||
After=network-online.target
|
After=network-online.target docker.service
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=oneshot
|
Type=oneshot
|
||||||
|
|||||||
@@ -318,7 +318,9 @@ def refresh_docker_rules():
|
|||||||
"""Apply the new settings to the Docker rules while Docker is running.
|
"""Apply the new settings to the Docker rules while Docker is running.
|
||||||
|
|
||||||
When Docker is not running, vpn-router-docker applies them after
|
When Docker is not running, vpn-router-docker applies them after
|
||||||
docker.service starts.
|
docker.service starts. The restart is waited for, so the rules are in place
|
||||||
|
when this setup ends; vpn-router-docker is therefore not ordered after
|
||||||
|
vpn-router-setup.
|
||||||
"""
|
"""
|
||||||
if unit_is_active('docker.service'):
|
if unit_is_active('docker.service'):
|
||||||
run(['systemctl', 'restart', 'vpn-router-docker.service'], check=False)
|
run(['systemctl', 'restart', 'vpn-router-docker.service'], check=False)
|
||||||
@@ -332,7 +334,8 @@ def refresh_web(cfg, ctx):
|
|||||||
secrets masked and the facts resolved from the system from a file here, so
|
secrets masked and the facts resolved from the system from a file here, so
|
||||||
that it needs nothing but the standard library and the configuration code.
|
that it needs nothing but the standard library and the configuration code.
|
||||||
The file is readable by root only. The service ends quietly when
|
The file is readable by root only. The service ends quietly when
|
||||||
web.enabled is false.
|
web.enabled is false. The restart is queued, not waited for, because the
|
||||||
|
unit is ordered after vpn-router-setup.
|
||||||
"""
|
"""
|
||||||
masked = {
|
masked = {
|
||||||
section: {option: '********' if SECRET_OPTION.search(option) and value else value
|
section: {option: '********' if SECRET_OPTION.search(option) and value else value
|
||||||
@@ -363,7 +366,7 @@ def refresh_web(cfg, ctx):
|
|||||||
'remote_cidrs': ctx['remote_cidr_list'],
|
'remote_cidrs': ctx['remote_cidr_list'],
|
||||||
},
|
},
|
||||||
}) + '\n', encoding='utf-8')
|
}) + '\n', encoding='utf-8')
|
||||||
run(['systemctl', 'restart', 'vpn-router-web.service'], check=False)
|
run(['systemctl', 'restart', '--no-block', 'vpn-router-web.service'], check=False)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import hashlib
|
|||||||
import hmac
|
import hmac
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
@@ -49,7 +48,6 @@ LEEWAY = 60
|
|||||||
MAX_LIFETIME = 600
|
MAX_LIFETIME = 600
|
||||||
|
|
||||||
TAILWIND = 'https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4'
|
TAILWIND = 'https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4'
|
||||||
SA_LINE = re.compile(r'^(\S+): #\d+, (\w+),')
|
|
||||||
|
|
||||||
_cache = {'at': 0.0, 'data': None}
|
_cache = {'at': 0.0, 'data': None}
|
||||||
_lock = threading.Lock()
|
_lock = threading.Lock()
|
||||||
@@ -85,13 +83,54 @@ def docker_status():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def established(connection):
|
def swanctl(command):
|
||||||
"""The number of IKE SAs of the connection, or None when swanctl cannot be asked."""
|
"""The entries of a swanctl list command, or None when swanctl cannot be asked.
|
||||||
result = run('swanctl', '--list-sas')
|
|
||||||
|
The --pretty output has one item per line: "name {" opens a section, "key = [" a list
|
||||||
|
whose items follow one per line, "}" and "]" close them, and "key = value" is a pair
|
||||||
|
whose value may contain spaces. Every "... event {" section at the top holds one
|
||||||
|
connection or SA; each becomes an entry with its name under "name". The closing
|
||||||
|
"... reply {" section is left out. Values stay strings, as swanctl prints them.
|
||||||
|
Output with more closing than opening lines gives None.
|
||||||
|
"""
|
||||||
|
result = run('swanctl', command, '--pretty')
|
||||||
if result is None or result.returncode != 0:
|
if result is None or result.returncode != 0:
|
||||||
return None
|
return None
|
||||||
return sum(1 for line in result.stdout.splitlines()
|
events = []
|
||||||
if (match := SA_LINE.match(line)) and match.group(1) == connection and match.group(2) == 'ESTABLISHED')
|
stack = []
|
||||||
|
for line in result.stdout.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
if line in ('}', ']'):
|
||||||
|
if not stack:
|
||||||
|
return None
|
||||||
|
stack.pop()
|
||||||
|
elif not stack:
|
||||||
|
section = {}
|
||||||
|
if line.endswith(' event {'):
|
||||||
|
events.append(section)
|
||||||
|
stack.append(section)
|
||||||
|
elif isinstance(stack[-1], list):
|
||||||
|
stack[-1].append(line)
|
||||||
|
elif line.endswith(' {'):
|
||||||
|
stack[-1][line[:-2]] = section = {}
|
||||||
|
stack.append(section)
|
||||||
|
elif line.endswith(' = ['):
|
||||||
|
stack[-1][line[:-4]] = items = []
|
||||||
|
stack.append(items)
|
||||||
|
else:
|
||||||
|
key, _, value = line.partition(' = ')
|
||||||
|
stack[-1][key] = value
|
||||||
|
return [dict(body, name=name) for event in events for name, body in event.items()]
|
||||||
|
|
||||||
|
|
||||||
|
def established(connection):
|
||||||
|
"""The number of established IKE SAs of the connection, or None when swanctl cannot be asked."""
|
||||||
|
sas = swanctl('--list-sas')
|
||||||
|
if sas is None:
|
||||||
|
return None
|
||||||
|
return sum(1 for sa in sas if sa['name'] == connection and sa.get('state') == 'ESTABLISHED')
|
||||||
|
|
||||||
|
|
||||||
def wireguard_peers():
|
def wireguard_peers():
|
||||||
@@ -176,6 +215,17 @@ def lines_of(*args):
|
|||||||
return result.stdout.splitlines() if result and result.returncode == 0 else None
|
return result.stdout.splitlines() if result and result.returncode == 0 else None
|
||||||
|
|
||||||
|
|
||||||
|
def routes():
|
||||||
|
"""The IPv4 routes as ip -j prints them, or None when ip fails."""
|
||||||
|
result = run('ip', '-j', '-4', 'route', 'show')
|
||||||
|
if result is None or result.returncode != 0:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return json.loads(result.stdout)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def wireguard_details():
|
def wireguard_details():
|
||||||
"""The interface and the peers of wg0, with endpoints and allowed IPs; no private or preshared key."""
|
"""The interface and the peers of wg0, with endpoints and allowed IPs; no private or preshared key."""
|
||||||
result = run('wg', 'show', 'wg0', 'dump')
|
result = run('wg', 'show', 'wg0', 'dump')
|
||||||
@@ -202,10 +252,10 @@ def details(state):
|
|||||||
configuration = state.get('configuration', {})
|
configuration = state.get('configuration', {})
|
||||||
return {
|
return {
|
||||||
'configuration': configuration,
|
'configuration': configuration,
|
||||||
'network': dict(state.get('network', {}), routes=lines_of('ip', '-4', 'route', 'show')),
|
'network': dict(state.get('network', {}), routes=routes()),
|
||||||
'ipsec': {
|
'ipsec': {
|
||||||
'connections': lines_of('swanctl', '--list-conns'),
|
'connections': swanctl('--list-conns'),
|
||||||
'sas': lines_of('swanctl', '--list-sas'),
|
'sas': swanctl('--list-sas'),
|
||||||
},
|
},
|
||||||
'wireguard': wireguard_details(),
|
'wireguard': wireguard_details(),
|
||||||
'docker': {
|
'docker': {
|
||||||
|
|||||||
Reference in new issue
Block a user