update: Added OpenLDAP and OpenVPN to the router.
Release / release (push) Successful in 18s

This commit is contained in:
slawek committed 2026-10-23 21:55:03 +02:00
1 parent 609921348b
commit cc6748a622
17 files changed
+1011 -63

No files matched your search

@@ -19,6 +19,7 @@ LIB_DIR = pathlib.Path(__file__).resolve().parent
if str(LIB_DIR) not in sys.path:
sys.path.insert(0, str(LIB_DIR))
import vpnrouter_ldap
import vpnrouter_platforms
CONFIG_FILE = pathlib.Path('/etc/vpn-router/vpn-router.json')
@@ -31,6 +32,9 @@ REMOTE_SITE = SWANCTL_CONF_D / 'remote-site.conf'
ROAD_WARRIOR = SWANCTL_CONF_D / 'road-warrior.conf'
P2S_FORWARDER = pathlib.Path('/etc/systemd/resolved.conf.d/p2s-forwarder.conf')
WG_CONF = pathlib.Path('/etc/wireguard/wg0.conf')
OPENVPN_CONF = pathlib.Path('/etc/openvpn/server/vpn-router.conf')
OPENVPN_LDAP = pathlib.Path('/etc/openvpn/server/vpn-router-ldap.conf')
USER_CERT_DIR = PKI_DIR / 'users'
CA_CERT_DEST = pathlib.Path('/etc/swanctl/x509ca/ca.pem')
SERVER_CERT = pathlib.Path('/etc/swanctl/x509/server.pem')
@@ -47,6 +51,7 @@ SCHEMA = {
'remote': {'addrs': [], 'id': '', 'cidrs': [], 'psk': '', 'psk_file': ''},
'p2s': {'enabled': False, 'address_pool': '', 'ca_name': 'VPN Router CA'},
'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': 8080, 'jwt_secret': ''},
}
@@ -56,6 +61,14 @@ WEB_SECRET_MIN = 32
DOCKER_MODES = ('auto', 'on', 'off')
OPENVPN_PROTOCOLS = ('udp', 'tcp')
#: The tun device of the OpenVPN server.
OPENVPN_DEVICE = 'ovpn0'
#: Days a certificate revocation list is valid; the router only reads its own lists.
CRL_DAYS = 3650
#: A port or a port range as iptables multiport takes it: 443 or 8000:8100.
DOCKER_PORT = re.compile(r'^\d{1,5}(:\d{1,5})?$')
@@ -209,6 +222,13 @@ def validate(cfg):
_check_networks(_get_list(cfg, 'remote', 'cidrs'), 'remote.cidrs')
pool = _get(cfg, 'p2s', 'address_pool')
_check_networks([pool] if pool else [], 'p2s.address_pool')
openvpn_pool = _get(cfg, 'openvpn', 'address_pool')
if openvpn_pool:
try:
if ipaddress.ip_network(openvpn_pool).version != 4:
raise ValueError
except ValueError:
raise ConfigError(f'openvpn.address_pool: {openvpn_pool!r} is not an IPv4 network')
_check_address(_get(cfg, 'local', 'int_addr'), 'local.int_addr')
_check_address(_get(cfg, 'local', 'int_gateway_ip'), 'local.int_gateway_ip')
@@ -232,6 +252,15 @@ def validate(cfg):
if not 1 <= port <= 65535:
raise ConfigError(f'wireguard.listen_port: {port} is not a valid port')
openvpn_port = _get(cfg, 'openvpn', 'port')
if not 1 <= openvpn_port <= 65535:
raise ConfigError(f'openvpn.port: {openvpn_port} is not a valid port')
protocol = _get(cfg, 'openvpn', 'protocol')
if protocol not in OPENVPN_PROTOCOLS:
raise ConfigError(
f'openvpn.protocol: {protocol!r} is not one of {", ".join(OPENVPN_PROTOCOLS)}')
web_port = _get(cfg, 'web', 'port')
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')
@@ -438,6 +467,8 @@ def build_context(cfg, local_id_override=None):
id_mode = _get(cfg, 'wan', 'local_id_mode') or 'fqdn'
local_cidrs = _get_list(cfg, 'local', 'cidrs')
remote_cidrs = _get_list(cfg, 'remote', 'cidrs')
openvpn_pool = _get(cfg, 'openvpn', 'address_pool')
openvpn_network = ipaddress.ip_network(openvpn_pool) if openvpn_pool else None
if local_id_override:
local_id = local_id_override
@@ -477,6 +508,23 @@ def build_context(cfg, local_id_override=None):
'wg_enabled': _get(cfg, 'wireguard', 'enabled'),
'wg_address': _get(cfg, 'wireguard', 'address'),
'wg_listen_port': _get(cfg, 'wireguard', 'listen_port'),
'openvpn_enabled': _get(cfg, 'openvpn', 'enabled'),
'openvpn_pool': openvpn_pool,
'openvpn_network': str(openvpn_network.network_address) if openvpn_network else '',
'openvpn_netmask': str(openvpn_network.netmask) if openvpn_network else '',
'openvpn_routes': [(str(network.network_address), str(network.netmask))
for network in (ipaddress.ip_network(cidr, strict=False) for cidr in local_cidrs)],
'openvpn_port': _get(cfg, 'openvpn', 'port'),
'openvpn_protocol': _get(cfg, 'openvpn', 'protocol'),
'openvpn_device': OPENVPN_DEVICE,
'openvpn_ldap_conf': str(OPENVPN_LDAP),
'p2s_dns_list': _split_list(_join(local_addrs, _split_list(extras['EXTRA_P2S_DNS']))),
'ca_cert': str(CA_CERT_DEST),
'server_cert': str(SERVER_CERT),
'server_key': str(SERVER_KEY),
'crl': str(CRL_DEST),
'ldap_uri': vpnrouter_ldap.URI,
'ldap_users': vpnrouter_ldap.USERS,
'web_enabled': _get(cfg, 'web', 'enabled'),
'web_port': _get(cfg, 'web', 'port'),
}
@@ -504,6 +552,10 @@ def wg_ready(ctx):
return bool(ctx['wg_enabled'] and ctx['wg_address'])
def openvpn_ready(ctx):
return bool(ctx['openvpn_enabled'] and ctx['openvpn_pool'] and ctx['local_fqdn'])
def _jinja_env():
return jinja2.Environment(
loader=jinja2.FileSystemLoader(str(TEMPLATE_DIR)),
@@ -593,11 +645,13 @@ def _simple_ca(*args):
def provision_pki(ctx):
"""Create the CA and server certificate when the PKI directory is empty."""
"""Create the CA, server certificate and CRL when the PKI directory is empty; a CA without a CRL gets one."""
PKI_DIR.mkdir(parents=True, exist_ok=True)
os.chmod(PKI_DIR, 0o700)
if any(PKI_DIR.iterdir()):
if (PKI_DIR / 'ca_key.pem').is_file() and not (PKI_DIR / 'crl.pem').is_file():
_simple_ca('make-crl', '--ca-dir', str(PKI_DIR), '--days', str(CRL_DAYS))
return
if not ctx['p2s_ca_name']:
warn('p2s.ca_name is empty, not creating a local CA')
@@ -608,6 +662,7 @@ def provision_pki(ctx):
_simple_ca('make-ca', '--ca-dir', str(PKI_DIR), ctx['p2s_ca_name'])
_simple_ca('make-cert', '--ca-dir', str(PKI_DIR),
ctx['local_fqdn'], ctx['local_fqdn'])
_simple_ca('make-crl', '--ca-dir', str(PKI_DIR), '--days', str(CRL_DAYS))
def distribute_pki(ctx):
@@ -635,7 +690,7 @@ def configure(cfg=None, local_id_override=None):
"""Render configuration files and provision PKI.
Returns (ctx, changed) where changed is a set naming what needs reloading:
'swanctl', 'resolved', 'wireguard'.
'swanctl', 'resolved', 'wireguard', 'openvpn'.
"""
if cfg is None:
cfg = load_config()
@@ -653,7 +708,7 @@ def configure(cfg=None, local_id_override=None):
changed = set()
if p2s_ready(ctx):
if p2s_ready(ctx) or openvpn_ready(ctx):
provision_pki(ctx)
if distribute_pki(ctx):
changed.add('swanctl')
@@ -669,13 +724,14 @@ def configure(cfg=None, local_id_override=None):
if p2s_ready(ctx):
if render(env, ctx, 'road-warrior.conf.j2', ROAD_WARRIOR, 0o600):
changed.add('swanctl')
elif remove(ROAD_WARRIOR):
changed.add('swanctl')
if p2s_ready(ctx) or openvpn_ready(ctx):
if render(env, ctx, 'p2s-forwarder.conf.j2', P2S_FORWARDER, 0o644):
changed.add('resolved')
else:
if remove(ROAD_WARRIOR):
changed.add('swanctl')
if remove(P2S_FORWARDER):
changed.add('resolved')
elif remove(P2S_FORWARDER):
changed.add('resolved')
if wg_ready(ctx):
if render_wg_conf(env, ctx):
@@ -683,4 +739,14 @@ def configure(cfg=None, local_id_override=None):
elif remove(WG_CONF):
changed.add('wireguard')
if openvpn_ready(ctx):
if render(env, ctx, 'openvpn-server.conf.j2', OPENVPN_CONF, 0o600):
changed.add('openvpn')
if render(env, ctx, 'openvpn-ldap.conf.j2', OPENVPN_LDAP, 0o600):
changed.add('openvpn')
else:
for path in (OPENVPN_CONF, OPENVPN_LDAP):
if remove(path):
changed.add('openvpn')
return ctx, changed