Converted router configuration file from .conf to JSON.

This commit is contained in:
slawek committed 2026-10-23 07:35:43 +02:00
1 parent c423ea9347
commit b18947ed9b
18 files changed
+273 -384

No files matched your search

+52 -41
View File
@@ -24,7 +24,7 @@ supported equally, and both end up with the same two things: a configuration fil
service that applies it. service that applies it.
``` ```
/etc/vpn-router/vpn-router.conf the configuration /etc/vpn-router/vpn-router.json the configuration
systemctl restart vpn-router-setup apply it systemctl restart vpn-router-setup apply it
``` ```
@@ -32,7 +32,7 @@ systemctl restart vpn-router-setup apply it
```sh ```sh
apt-get install vpn-router # answer "none" to the platform question apt-get install vpn-router # answer "none" to the platform question
$EDITOR /etc/vpn-router/vpn-router.conf $EDITOR /etc/vpn-router/vpn-router.json
systemctl restart vpn-router-setup systemctl restart vpn-router-setup
``` ```
@@ -65,10 +65,10 @@ EOF
DEBIAN_FRONTEND=noninteractive apt-get install -y vpn-router DEBIAN_FRONTEND=noninteractive apt-get install -y vpn-router
``` ```
With `mode = interfaces` the `int_addr` and `int_gateway_ip` lines are dropped and read With mode `interfaces` the `int_addr` and `int_gateway_ip` lines are dropped and read
from the internal NIC instead; with `mode = auto` the interface names go too. from the internal NIC instead; with mode `auto` the interface names go too.
Alternatively write `/etc/vpn-router/vpn-router.conf` directly before or after installing. Alternatively write `/etc/vpn-router/vpn-router.json` directly before or after installing.
A configuration-management tool should do that and then restart `vpn-router-setup`. A configuration-management tool should do that and then restart `vpn-router-setup`.
See [examples/](examples/) for a cloud-init template and a shell installer, and See [examples/](examples/) for a cloud-init template and a shell installer, and
@@ -77,47 +77,58 @@ that deploy a router on Azure or GCP.
## Configuration ## Configuration
`/etc/vpn-router/vpn-router.conf` is INI, mode 0600, and is created on first install. It `/etc/vpn-router/vpn-router.json` is JSON, mode 0600, and is created on first install. It
is managed with `ucf`, so it is yours to edit: a later `dpkg-reconfigure` applies what is managed with `ucf`, so it is yours to edit: a later `dpkg-reconfigure` applies what
changed and asks before touching anything you altered by hand. A fully commented changed and asks before touching anything you altered by hand. A complete example is
reference is installed at `/usr/share/doc/vpn-router/vpn-router.conf.example`. installed at `/usr/share/doc/vpn-router/vpn-router.json.example`.
Lists are comma-separated, booleans are `true`/`false`, an empty value means unset, and a The file is an object of sections, each an object of settings. A section or setting that
`_b64` suffix means the value is base64-encoded. is left out takes its default, and an empty string or empty list means unset. Every value
must have the type shown below; an unknown section or setting is an error.
| Section | Setting | Meaning | | Section | Setting | Type | Default | Meaning |
|---|---|---| |---|---|---|---|---|
| `general` | `platform` | `none`, `generic`, `azure` or `gcp`. `none` defers configuration entirely | | `general` | `platform` | string | `none` | `none`, `generic`, `azure` or `gcp`. `none` defers configuration entirely |
| `general` | `mode` | How much you supply: `manual`, `interfaces` or `auto` | | `general` | `mode` | string | `manual` | How much you supply: `manual`, `interfaces` or `auto` |
| `interfaces` | `external` | Name of the NIC facing the untrusted network | | `interfaces` | `external` | string | | Name of the NIC facing the untrusted network |
| `interfaces` | `internal` | Name of the NIC facing the protected network | | `interfaces` | `internal` | string | | Name of the NIC facing the protected network |
| `wan` | `local_fqdn` | This router's FQDN | | `wan` | `local_fqdn` | string | | This router's FQDN |
| `wan` | `local_id_mode` | IKE identity source: `fqdn`, `public_ip` or `internal_ip` | | `wan` | `local_id_mode` | string | `fqdn` | IKE identity source: `fqdn`, `public_ip` or `internal_ip` |
| `local` | `cidrs` | Local subnets advertised into the tunnel | | `local` | `cidrs` | list | | Local subnets advertised into the tunnel |
| `local` | `int_addr` | This host's address on the internal network | | `local` | `int_addr` | string | | This host's address on the internal network |
| `local` | `int_gateway_ip` | Next hop on the internal side | | `local` | `int_gateway_ip` | string | | Next hop on the internal side |
| `remote` | `addrs` | Remote gateway address(es) or FQDN | | `remote` | `addrs` | list | | Remote gateway addresses or FQDNs |
| `remote` | `id` | Remote IKE identity, without a leading `@` | | `remote` | `id` | string | | Remote IKE identity, without a leading `@` |
| `remote` | `cidrs` | Remote subnets reachable through the tunnel | | `remote` | `cidrs` | list | | Remote subnets reachable through the tunnel |
| `remote` | `psk_b64` | Pre-shared key, base64-encoded | | `remote` | `psk` | string | | Pre-shared key |
| `remote` | `psk_file` | Path to a file holding the raw key; wins over `psk_b64` | | `remote` | `psk_file` | string | | Path to a file holding the key; wins over `psk` |
| `p2s` | `enabled` | Road-warrior access | | `p2s` | `enabled` | boolean | `false` | Road-warrior access |
| `p2s` | `address_pool` | Pool assigned to road-warrior clients | | `p2s` | `address_pool` | string | | CIDR assigned to road-warrior clients |
| `p2s` | `ca_name` | Common name for a locally created CA | | `p2s` | `ca_name` | string | `VPN Router CA` | Common name for a locally created CA |
| `wireguard` | `enabled` | WireGuard endpoint | | `wireguard` | `enabled` | boolean | `false` | WireGuard endpoint |
| `wireguard` | `address` | Address and prefix for `wg0` | | `wireguard` | `address` | string | | Address and prefix for `wg0`, for example `192.168.200.1/24` |
| `wireguard` | `listen_port` | UDP port, default 51820 | | `wireguard` | `listen_port` | integer | `51820` | UDP port |
A minimal site-to-site configuration in `auto` mode:
```json
{
"general": { "platform": "generic", "mode": "auto" },
"wan": { "local_fqdn": "router.example.com" },
"local": { "cidrs": ["10.0.0.0/24"] },
"remote": {
"addrs": ["peer.example.net"],
"id": "peer.example.net",
"cidrs": ["192.168.0.0/24"],
"psk": "s3cr3t"
}
}
```
WireGuard peers are added by hand in `/etc/wireguard/wg0.conf`. The package owns the WireGuard peers are added by hand in `/etc/wireguard/wg0.conf`. The package owns the
`[Interface]` section of that file and rewrites it when the settings above change, but it `[Interface]` section of that file and rewrites it when the settings above change, but it
carries every `[Peer]` section across untouched. carries every `[Peer]` section across untouched.
Encode the pre-shared key with:
```sh
printf %s 'the key' | base64 -w0
```
Every feature is independent. Leaving a group empty simply means that feature is not Every feature is independent. Leaving a group empty simply means that feature is not
configured, and its generated files are removed. configured, and its generated files are removed.
@@ -148,7 +159,7 @@ address of the connected subnet with a warning when the routing table has nothin
systemctl restart vpn-router-setup systemctl restart vpn-router-setup
``` ```
The service also runs at boot. With `platform = none` it does nothing and says so. The service also runs at boot. With platform `none` it does nothing and says so.
Otherwise it regenerates the configuration files, resolves the interfaces and their Otherwise it regenerates the configuration files, resolves the interfaces and their
addresses as `mode` directs, applies routes, generates WireGuard keys and injects addresses as `mode` directs, applies routes, generates WireGuard keys and injects
firewall rules. It then starts `strongswan` and `wg-quick@wg0` if the corresponding firewall rules. It then starts `strongswan` and `wg-quick@wg0` if the corresponding
@@ -240,7 +251,7 @@ EXTRA_P2S_DNS = '192.0.2.53'
EXTRA_LOCAL_TS = '192.0.2.53/32' EXTRA_LOCAL_TS = '192.0.2.53/32'
``` ```
Set `platform = example` and restart the service. The debconf question offers only the Set `general.platform` to `example` and restart the service. The debconf question offers only the
modules shipped with the package; the configuration file accepts any module installed. modules shipped with the package; the configuration file accepts any module installed.
## Removal ## Removal
@@ -250,7 +261,7 @@ configuration in place. `strongswan` is left enabled, since it is a shared servi
may be in use by something else. may be in use by something else.
`apt-get purge` additionally removes the generated files and the firewall rules, restoring `apt-get purge` additionally removes the generated files and the firewall rules, restoring
`/etc/ufw/before.rules` to its original content, and hands `vpn-router.conf` back to `ucf` `/etc/ufw/before.rules` to its original content, and hands `vpn-router.json` back to `ucf`
before deleting it so a later reinstall starts clean. Key material is never deleted: before deleting it so a later reinstall starts clean. Key material is never deleted:
`/etc/vpn-router/pki`, the WireGuard key pair and everything under `/etc/swanctl` are left `/etc/vpn-router/pki`, the WireGuard key pair and everything under `/etc/swanctl` are left
alone. alone.
View File
Whitespace-only changes.
+1 -1
View File
@@ -28,7 +28,7 @@ Description: Linux VPN router with IPSec, road-warrior and WireGuard support
optional WireGuard, and the routing, NAT, MSS clamping and firewall rules a optional WireGuard, and the routing, NAT, MSS clamping and firewall rules a
router needs. Nothing in the package is specific to a cloud provider. router needs. Nothing in the package is specific to a cloud provider.
. .
Settings live in /etc/vpn-router/vpn-router.conf, which is created on first Settings live in /etc/vpn-router/vpn-router.json, which is created on first
install and owned by the administrator afterwards. Editing it and restarting install and owned by the administrator afterwards. Editing it and restarting
vpn-router-setup applies the change; the same service reconciles the system vpn-router-setup applies the change; the same service reconciles the system
at every boot. It is built on two inputs, the external and internal interface at every boot. It is built on two inputs, the external and internal interface
+1 -1
View File
@@ -6,4 +6,4 @@ src/usr/lib/vpn-router/setup usr/lib/vpn-router/
src/usr/lib/vpn-router/generate-config usr/lib/vpn-router/ src/usr/lib/vpn-router/generate-config usr/lib/vpn-router/
src/usr/lib/vpn-router/vpnrouter_platforms/*.py usr/lib/vpn-router/vpnrouter_platforms/ src/usr/lib/vpn-router/vpnrouter_platforms/*.py usr/lib/vpn-router/vpnrouter_platforms/
src/usr/share/vpn-router/templates/* usr/share/vpn-router/templates/ src/usr/share/vpn-router/templates/* usr/share/vpn-router/templates/
src/usr/share/doc/vpn-router/vpn-router.conf.example usr/share/doc/vpn-router/ src/usr/share/doc/vpn-router/vpn-router.json.example usr/share/doc/vpn-router/
+3 -3
View File
@@ -42,9 +42,9 @@ case "$1" in
# This is what makes dpkg-reconfigure apply without destroying edits. # This is what makes dpkg-reconfigure apply without destroying edits.
CANDIDATE="$(mktemp)" CANDIDATE="$(mktemp)"
/usr/lib/vpn-router/generate-config "$CANDIDATE" /usr/lib/vpn-router/generate-config "$CANDIDATE"
ucf --three-way --debconf-ok "$CANDIDATE" /etc/vpn-router/vpn-router.conf ucf --three-way --debconf-ok "$CANDIDATE" /etc/vpn-router/vpn-router.json
ucfr vpn-router /etc/vpn-router/vpn-router.conf ucfr vpn-router /etc/vpn-router/vpn-router.json
chmod 0600 /etc/vpn-router/vpn-router.conf chmod 0600 /etc/vpn-router/vpn-router.json
rm -f "$CANDIDATE" rm -f "$CANDIDATE"
# The key now lives in the configuration file; do not keep a copy. An # The key now lives in the configuration file; do not keep a copy. An
+3 -3
View File
@@ -37,17 +37,17 @@ case "$1" in
# Let ucf forget the file before it is removed, or a reinstall finds a # Let ucf forget the file before it is removed, or a reinstall finds a
# stale hash and declines to lay the file down again. # stale hash and declines to lay the file down again.
if command -v ucf >/dev/null 2>&1; then if command -v ucf >/dev/null 2>&1; then
ucf --purge /etc/vpn-router/vpn-router.conf ucf --purge /etc/vpn-router/vpn-router.json
fi fi
if command -v ucfr >/dev/null 2>&1; then if command -v ucfr >/dev/null 2>&1; then
ucfr --purge vpn-router /etc/vpn-router/vpn-router.conf ucfr --purge vpn-router /etc/vpn-router/vpn-router.json
fi fi
rm -f /etc/swanctl/conf.d/remote-site.conf \ rm -f /etc/swanctl/conf.d/remote-site.conf \
/etc/swanctl/conf.d/road-warrior.conf \ /etc/swanctl/conf.d/road-warrior.conf \
/etc/systemd/resolved.conf.d/p2s-forwarder.conf \ /etc/systemd/resolved.conf.d/p2s-forwarder.conf \
/etc/wireguard/wg0.conf \ /etc/wireguard/wg0.conf \
/etc/vpn-router/vpn-router.conf /etc/vpn-router/vpn-router.json
# Key material is not package state: /etc/vpn-router/pki, the WireGuard # Key material is not package state: /etc/vpn-router/pki, the WireGuard
# key pair and everything under /etc/swanctl are left in place. # key pair and everything under /etc/swanctl are left in place.
+3 -3
View File
@@ -9,7 +9,7 @@ Description: Platform:
none defers configuration entirely: the package installs its files and none defers configuration entirely: the package installs its files and
changes nothing on the machine. Choose it when the router will be configured changes nothing on the machine. Choose it when the router will be configured
later by hand or by a configuration-management tool, then set the platform in later by hand or by a configuration-management tool, then set the platform in
/etc/vpn-router/vpn-router.conf when you are ready. /etc/vpn-router/vpn-router.json when you are ready.
. .
generic configures the router with no platform-specific additions. generic configures the router with no platform-specific additions.
@@ -113,8 +113,8 @@ Template: vpn-router/psk
Type: password Type: password
Description: Pre-shared key (PSK): Description: Pre-shared key (PSK):
Pre-shared key for the site-to-site IKEv2 tunnel. Must match the value Pre-shared key for the site-to-site IKEv2 tunnel. Must match the value
configured on the remote peer. Stored base64-encoded in the configuration configured on the remote peer. Stored in the configuration file and cleared
file and cleared from the debconf database after installation. from the debconf database after installation.
Template: vpn-router/p2s_enabled Template: vpn-router/p2s_enabled
Type: boolean Type: boolean
+1 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/python3 #!/usr/bin/python3
"""Render configuration files and provision PKI from vpn-router.conf. """Render configuration files and provision PKI from vpn-router.json.
Run directly to regenerate them without starting, stopping or reloading Run directly to regenerate them without starting, stopping or reloading
anything. This still writes to /etc/swanctl, /etc/systemd/resolved.conf.d and anything. This still writes to /etc/swanctl, /etc/systemd/resolved.conf.d and
@@ -1,15 +1,15 @@
#!/usr/bin/python3 #!/usr/bin/python3
"""Write a candidate vpn-router.conf to the path given on the command line. """Write a candidate vpn-router.json to the path given on the command line.
Called from postinst, which hands the result to ucf(1). ucf compares it with Called from postinst, which hands the result to ucf(1). ucf compares it with
/etc/vpn-router/vpn-router.conf and decides what to do about local changes, so /etc/vpn-router/vpn-router.json and decides what to do about local changes, so
this script never writes to /etc and never has to worry about destroying an this script never writes to /etc and never has to worry about destroying an
administrator's edits. administrator's edits.
The candidate is built in three layers, each overriding the one before: The candidate is built in three layers, each overriding the one before:
1. the shipped defaults 1. the shipped defaults
2. the current contents of /etc/vpn-router/vpn-router.conf, if it exists 2. the current contents of /etc/vpn-router/vpn-router.json, if it exists
3. any debconf answer that was actually given 3. any debconf answer that was actually given
Layer 2 is what makes reconfiguration safe: a setting the administrator edited Layer 2 is what makes reconfiguration safe: a setting the administrator edited
@@ -19,12 +19,12 @@ answer means "leave alone", which is why clearing the pre-shared key from the
debconf database after installation does not blank it on the next debconf database after installation does not blank it on the next
dpkg-reconfigure. dpkg-reconfigure.
Answers arrive as VPN_ROUTER_* environment variables. The pre-shared key is Answers arrive as VPN_ROUTER_* environment variables and are converted to the
stored base64-encoded so that arbitrary characters cannot collide with INI type of the setting they fill: comma-separated lists become arrays, "true" and
syntax. "false" become booleans and digits become integers.
""" """
import base64 import json
import os import os
import sys import sys
@@ -46,6 +46,7 @@ ANSWERS = {
'VPN_ROUTER_REMOTE_ADDRS': ('remote', 'addrs'), 'VPN_ROUTER_REMOTE_ADDRS': ('remote', 'addrs'),
'VPN_ROUTER_REMOTE_ID': ('remote', 'id'), 'VPN_ROUTER_REMOTE_ID': ('remote', 'id'),
'VPN_ROUTER_REMOTE_CIDRS': ('remote', 'cidrs'), 'VPN_ROUTER_REMOTE_CIDRS': ('remote', 'cidrs'),
'VPN_ROUTER_PSK': ('remote', 'psk'),
'VPN_ROUTER_P2S_ENABLED': ('p2s', 'enabled'), 'VPN_ROUTER_P2S_ENABLED': ('p2s', 'enabled'),
'VPN_ROUTER_P2S_ADDRESS_POOL': ('p2s', 'address_pool'), 'VPN_ROUTER_P2S_ADDRESS_POOL': ('p2s', 'address_pool'),
'VPN_ROUTER_P2S_CA_NAME': ('p2s', 'ca_name'), 'VPN_ROUTER_P2S_CA_NAME': ('p2s', 'ca_name'),
@@ -61,25 +62,25 @@ def main():
return 2 return 2
output = sys.argv[1] output = sys.argv[1]
parser = vpnrouter.default_parser() cfg = vpnrouter.load_config()
current = vpnrouter.load_config()
for section in parser.sections():
for option in parser[section]:
if current.has_option(section, option):
parser[section][option] = current.get(section, option)
for variable, (section, option) in ANSWERS.items(): for variable, (section, option) in ANSWERS.items():
if os.environ.get(variable): value = os.environ.get(variable)
parser[section][option] = os.environ[variable] if not value:
continue
psk = os.environ.get('VPN_ROUTER_PSK', '') # A value that does not convert is kept as a string for validate() to report.
if psk: default = vpnrouter.SCHEMA[section][option]
parser['remote']['psk_b64'] = base64.b64encode( if isinstance(default, bool):
psk.encode('utf-8')).decode('ascii') value = value == 'true'
elif isinstance(default, int):
value = int(value) if value.isdigit() else value
elif isinstance(default, list):
value = [item.strip() for item in value.split(',') if item.strip()]
cfg[section][option] = value
with open(output, 'w', encoding='utf-8') as handle: with open(output, 'w', encoding='utf-8') as handle:
parser.write(handle) json.dump(cfg, handle, indent=2)
handle.write('\n')
os.chmod(output, 0o600) os.chmod(output, 0o600)
return 0 return 0
+4 -4
View File
@@ -139,10 +139,10 @@ def filter_block(ctx):
'# WIREGUARD RULES END', '# WIREGUARD RULES END',
] ]
if vpnrouter.p2s_ready(ctx) and ctx['local_addrs']: if vpnrouter.p2s_ready(ctx) and ctx['local_addr_list']:
pool = ctx['p2s_address_pool'] pool = ctx['p2s_address_pool']
lines += ['', '# P2S DNS RULES START'] lines += ['', '# P2S DNS RULES START']
for address in [a.strip() for a in ctx['local_addrs'].split(',') if a.strip()]: for address in ctx['local_addr_list']:
lines += [ lines += [
f'-A ufw-before-input -s {pool} -d {address} -p udp --dport 53 -j ACCEPT', f'-A ufw-before-input -s {pool} -d {address} -p udp --dport 53 -j ACCEPT',
f'-A ufw-before-input -s {pool} -d {address} -p tcp --dport 53 -j ACCEPT', f'-A ufw-before-input -s {pool} -d {address} -p tcp --dport 53 -j ACCEPT',
@@ -166,7 +166,7 @@ def nat_and_mangle_block(ctx):
if vpnrouter.routing_ready(ctx): if vpnrouter.routing_ready(ctx):
subnets = ctx['local_cidr_list'] subnets = ctx['local_cidr_list']
remote = [c.strip() for c in ctx['remote_cidrs'].split(',') if c.strip()] remote = ctx['remote_cidr_list']
lines += [ lines += [
'', '',
'# ROUTER NAT RULES START', '# ROUTER NAT RULES START',
@@ -305,7 +305,7 @@ def main():
if ctx is None: if ctx is None:
print('vpn-router: platform is none, configuration deferred; ' print('vpn-router: platform is none, configuration deferred; '
'set general.platform in /etc/vpn-router/vpn-router.conf') f'set general.platform in {vpnrouter.CONFIG_FILE}')
return 0 return 0
if vpnrouter.routing_ready(ctx): if vpnrouter.routing_ready(ctx):
@@ -4,10 +4,8 @@ Everything derivable from the configuration file alone, with no running
network required. Used by both 'configure' and 'setup'. network required. Used by both 'configure' and 'setup'.
""" """
import base64
import binascii
import configparser
import ipaddress import ipaddress
import json
import os import os
import pathlib import pathlib
import shutil import shutil
@@ -22,7 +20,7 @@ if str(LIB_DIR) not in sys.path:
import vpnrouter_platforms import vpnrouter_platforms
CONFIG_FILE = pathlib.Path('/etc/vpn-router/vpn-router.conf') 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')
PKI_DIR = pathlib.Path('/etc/vpn-router/pki') PKI_DIR = pathlib.Path('/etc/vpn-router/pki')
SIMPLE_CA = LIB_DIR / 'simple-ca' SIMPLE_CA = LIB_DIR / 'simple-ca'
@@ -39,17 +37,20 @@ SERVER_KEY = pathlib.Path('/etc/swanctl/private/server.key')
CRL_DEST = pathlib.Path('/etc/swanctl/x509crl/crl.pem') CRL_DEST = pathlib.Path('/etc/swanctl/x509crl/crl.pem')
TRUST_ANCHOR = pathlib.Path('/usr/local/share/ca-certificates/vpn-router-ca.crt') TRUST_ANCHOR = pathlib.Path('/usr/local/share/ca-certificates/vpn-router-ca.crt')
# section -> option -> default # section -> option -> default; the type of the default is the required type.
SCHEMA = { SCHEMA = {
'general': {'platform': 'none', 'mode': 'manual'}, 'general': {'platform': 'none', 'mode': 'manual'},
'interfaces': {'external': '', 'internal': ''}, 'interfaces': {'external': '', 'internal': ''},
'wan': {'local_fqdn': '', 'local_id_mode': 'fqdn'}, 'wan': {'local_fqdn': '', 'local_id_mode': 'fqdn'},
'local': {'cidrs': '', 'int_addr': '', 'int_gateway_ip': ''}, 'local': {'cidrs': [], 'int_addr': '', 'int_gateway_ip': ''},
'remote': {'addrs': '', 'id': '', 'cidrs': '', 'psk_b64': '', 'psk_file': ''}, 'remote': {'addrs': [], 'id': '', 'cidrs': [], 'psk': '', 'psk_file': ''},
'p2s': {'enabled': 'false', 'address_pool': '', 'ca_name': 'VPN Router CA'}, 'p2s': {'enabled': False, 'address_pool': '', 'ca_name': 'VPN Router CA'},
'wireguard': {'enabled': 'false', 'address': '', 'listen_port': '51820'}, 'wireguard': {'enabled': False, 'address': '', 'listen_port': 51820},
} }
TYPE_NAMES = {str: 'a string', list: 'a list of strings', bool: 'a boolean',
int: 'an integer'}
ID_MODES = ('fqdn', 'public_ip', 'internal_ip') ID_MODES = ('fqdn', 'public_ip', 'internal_ip')
#: How much the operator supplies, and therefore how much may be detected. #: How much the operator supplies, and therefore how much may be detected.
@@ -77,46 +78,29 @@ def fail(message):
sys.exit(1) sys.exit(1)
def new_parser():
"""A parser that tolerates operator comments and never interpolates.
interpolation=None matters: the default BasicInterpolation treats '%' as
syntax, which would break any value containing it.
Inline comments are enabled, which configparser leaves off by default
because it makes '#' and ';' unusable inside values. That is an acceptable
trade here precisely because the one setting that can hold arbitrary
characters, the pre-shared key, is base64-encoded, and base64 uses none of
those characters.
"""
return configparser.ConfigParser(
interpolation=None,
comment_prefixes=('#', ';'),
inline_comment_prefixes=('#', ';'),
)
def default_parser():
parser = new_parser()
for section, options in SCHEMA.items():
parser[section] = dict(options)
return parser
def load_config(path=None): def load_config(path=None):
"""Read the configuration, filling in defaults for anything absent. """Read the configuration, filling in defaults for anything absent.
The result maps section -> option -> value. Sections and options not in
SCHEMA are kept so that validate() can report them.
The path is resolved when called, not when this function is defined, so The path is resolved when called, not when this function is defined, so
CONFIG_FILE stays overridable. CONFIG_FILE stays overridable.
""" """
path = pathlib.Path(path) if path else CONFIG_FILE path = pathlib.Path(path) if path else CONFIG_FILE
parser = default_parser() cfg = {section: dict(options) for section, options in SCHEMA.items()}
if path.exists(): if path.exists():
try: try:
parser.read(path, encoding='utf-8') data = json.loads(path.read_text(encoding='utf-8'))
except configparser.Error as exc: except ValueError as exc:
fail(f'cannot parse {path}: {exc}') fail(f'cannot parse {path}: {exc}')
return parser if not isinstance(data, dict):
fail(f'cannot parse {path}: the top level is not an object')
for section, options in data.items():
if not isinstance(options, dict):
fail(f'cannot parse {path}: {section} is not an object')
cfg.setdefault(section, {}).update(options)
return cfg
def _split_list(value): def _split_list(value):
@@ -124,19 +108,31 @@ def _split_list(value):
def _get(cfg, section, option): def _get(cfg, section, option):
return cfg.get(section, option, fallback=SCHEMA[section][option]).strip() value = cfg[section][option]
return value.strip() if isinstance(value, str) else value
def _get_bool(cfg, section, option): def _get_list(cfg, section, option):
try: return [item.strip() for item in cfg[section][option] if item.strip()]
return cfg.getboolean(section, option,
fallback=SCHEMA[section][option] == 'true')
except ValueError:
raise ConfigError(f'{section}.{option} is not a boolean')
def _check_networks(value, name): def _check_types(cfg):
for item in _split_list(value): for section, options in cfg.items():
if section not in SCHEMA:
raise ConfigError(f'{section}: unknown section')
for option, value in options.items():
if option not in SCHEMA[section]:
raise ConfigError(f'{section}.{option}: unknown setting')
expected = type(SCHEMA[section][option])
if type(value) is not expected or (
expected is list and
not all(isinstance(item, str) for item in value)):
raise ConfigError(
f'{section}.{option} must be {TYPE_NAMES[expected]}')
def _check_networks(values, name):
for item in values:
try: try:
ipaddress.ip_network(item, strict=False) ipaddress.ip_network(item, strict=False)
except ValueError: except ValueError:
@@ -158,6 +154,8 @@ def validate(cfg):
The exception is general.mode, which states what the operator promised to The exception is general.mode, which states what the operator promised to
supply, so the values that mode requires are checked for presence too. supply, so the values that mode requires are checked for presence too.
""" """
_check_types(cfg)
platform = _get(cfg, 'general', 'platform') platform = _get(cfg, 'general', 'platform')
installed = [PLATFORM_NONE] + vpnrouter_platforms.available() installed = [PLATFORM_NONE] + vpnrouter_platforms.available()
if platform and platform not in installed: if platform and platform not in installed:
@@ -187,9 +185,10 @@ def validate(cfg):
raise ConfigError( raise ConfigError(
f'wan.local_id_mode: {id_mode!r} is not one of {", ".join(ID_MODES)}') f'wan.local_id_mode: {id_mode!r} is not one of {", ".join(ID_MODES)}')
_check_networks(_get(cfg, 'local', 'cidrs'), 'local.cidrs') _check_networks(_get_list(cfg, 'local', 'cidrs'), 'local.cidrs')
_check_networks(_get(cfg, 'remote', 'cidrs'), 'remote.cidrs') _check_networks(_get_list(cfg, 'remote', 'cidrs'), 'remote.cidrs')
_check_networks(_get(cfg, 'p2s', 'address_pool'), 'p2s.address_pool') pool = _get(cfg, 'p2s', 'address_pool')
_check_networks([pool] if pool else [], 'p2s.address_pool')
_check_address(_get(cfg, 'local', 'int_addr'), 'local.int_addr') _check_address(_get(cfg, 'local', 'int_addr'), 'local.int_addr')
_check_address(_get(cfg, 'local', 'int_gateway_ip'), 'local.int_gateway_ip') _check_address(_get(cfg, 'local', 'int_gateway_ip'), 'local.int_gateway_ip')
@@ -210,12 +209,8 @@ def validate(cfg):
f'wireguard.address: {wg_address!r} is not an address with prefix') f'wireguard.address: {wg_address!r} is not an address with prefix')
port = _get(cfg, 'wireguard', 'listen_port') port = _get(cfg, 'wireguard', 'listen_port')
if port: if not 1 <= port <= 65535:
if not port.isdigit() or not 1 <= int(port) <= 65535: raise ConfigError(f'wireguard.listen_port: {port} is not a valid port')
raise ConfigError(f'wireguard.listen_port: {port!r} is not a valid port')
for section in ('p2s', 'wireguard'):
_get_bool(cfg, section, 'enabled')
def resolve_psk(cfg): def resolve_psk(cfg):
@@ -226,14 +221,7 @@ def resolve_psk(cfg):
if not path.is_file(): if not path.is_file():
raise ConfigError(f'remote.psk_file: {psk_file} does not exist') raise ConfigError(f'remote.psk_file: {psk_file} does not exist')
return path.read_text(encoding='utf-8').strip() return path.read_text(encoding='utf-8').strip()
return cfg['remote']['psk']
psk_b64 = _get(cfg, 'remote', 'psk_b64')
if not psk_b64:
return ''
try:
return base64.b64decode(psk_b64, validate=True).decode('utf-8')
except (binascii.Error, UnicodeDecodeError) as exc:
raise ConfigError(f'remote.psk_b64 is not valid base64 text: {exc}')
def platform_module(cfg): def platform_module(cfg):
@@ -253,7 +241,7 @@ def platform_module(cfg):
def _join(*groups): def _join(*groups):
items = [] items = []
for group in groups: for group in groups:
for item in _split_list(group): for item in group:
if item not in items: if item not in items:
items.append(item) items.append(item)
return ', '.join(items) return ', '.join(items)
@@ -369,9 +357,8 @@ def resolve_interfaces(cfg):
if mode == 'auto': if mode == 'auto':
ext_iface, int_iface = detect_interfaces() ext_iface, int_iface = detect_interfaces()
ext_addrs = interface_addresses(ext_iface, 'interfaces.external') local_addrs = interface_addresses(ext_iface, 'interfaces.external')
local_addrs = ', '.join(ext_addrs) if ext_iface and not local_addrs:
if ext_iface and not ext_addrs:
warn(f'{ext_iface} has no IPv4 address yet') warn(f'{ext_iface} has no IPv4 address yet')
if mode == 'manual': if mode == 'manual':
@@ -393,7 +380,8 @@ def build_context(cfg, local_id_override=None):
local_fqdn = _get(cfg, 'wan', 'local_fqdn') local_fqdn = _get(cfg, 'wan', 'local_fqdn')
id_mode = _get(cfg, 'wan', 'local_id_mode') or 'fqdn' id_mode = _get(cfg, 'wan', 'local_id_mode') or 'fqdn'
local_cidrs = _get(cfg, 'local', 'cidrs') local_cidrs = _get_list(cfg, 'local', 'cidrs')
remote_cidrs = _get_list(cfg, 'remote', 'cidrs')
if local_id_override: if local_id_override:
local_id = local_id_override local_id = local_id_override
@@ -406,32 +394,31 @@ def build_context(cfg, local_id_override=None):
# re-renders with local_id_override set. # re-renders with local_id_override set.
local_id = '' local_id = ''
cidr_list = _split_list(local_cidrs)
return { return {
'platform': _get(cfg, 'general', 'platform') or PLATFORM_NONE, 'platform': _get(cfg, 'general', 'platform') or PLATFORM_NONE,
'mode': _get(cfg, 'general', 'mode') or 'manual', 'mode': _get(cfg, 'general', 'mode') or 'manual',
'wan_iface': ext_iface, 'wan_iface': ext_iface,
'int_iface': int_iface, 'int_iface': int_iface,
'local_addrs': local_addrs, 'local_addrs': ', '.join(local_addrs),
'local_addr_list': _split_list(local_addrs), 'local_addr_list': local_addrs,
'local_fqdn': local_fqdn, 'local_fqdn': local_fqdn,
'local_id_mode': id_mode, 'local_id_mode': id_mode,
'local_id': local_id, 'local_id': local_id,
'local_cidrs': local_cidrs, 'local_cidrs': ', '.join(local_cidrs),
'local_cidr_list': cidr_list, 'local_cidr_list': local_cidrs,
'local_ts': _join(local_cidrs, extras['EXTRA_LOCAL_TS']), 'local_ts': _join(local_cidrs, _split_list(extras['EXTRA_LOCAL_TS'])),
'int_addr': int_addr, 'int_addr': int_addr,
'int_gateway_ip': gateway, 'int_gateway_ip': gateway,
'remote_addrs': _get(cfg, 'remote', 'addrs'), 'remote_addrs': ', '.join(_get_list(cfg, 'remote', 'addrs')),
'remote_id': _get(cfg, 'remote', 'id'), 'remote_id': _get(cfg, 'remote', 'id'),
'remote_cidrs': _get(cfg, 'remote', 'cidrs'), 'remote_cidrs': ', '.join(remote_cidrs),
'remote_cidr_list': remote_cidrs,
'psk': resolve_psk(cfg), 'psk': resolve_psk(cfg),
'p2s_enabled': _get_bool(cfg, 'p2s', 'enabled'), 'p2s_enabled': _get(cfg, 'p2s', 'enabled'),
'p2s_address_pool': _get(cfg, 'p2s', 'address_pool'), 'p2s_address_pool': _get(cfg, 'p2s', 'address_pool'),
'p2s_ca_name': _get(cfg, 'p2s', 'ca_name'), 'p2s_ca_name': _get(cfg, 'p2s', 'ca_name'),
'p2s_dns': _join(local_addrs, extras['EXTRA_P2S_DNS']), 'p2s_dns': _join(local_addrs, _split_list(extras['EXTRA_P2S_DNS'])),
'wg_enabled': _get_bool(cfg, 'wireguard', 'enabled'), 'wg_enabled': _get(cfg, 'wireguard', 'enabled'),
'wg_address': _get(cfg, 'wireguard', 'address'), 'wg_address': _get(cfg, 'wireguard', 'address'),
'wg_listen_port': _get(cfg, 'wireguard', 'listen_port'), 'wg_listen_port': _get(cfg, 'wireguard', 'listen_port'),
} }
@@ -1,84 +0,0 @@
# Example vpn-router configuration.
#
# This file is documentation. The live configuration is
# /etc/vpn-router/vpn-router.conf, which is generated on first install and
# owned by the operator afterwards. Copy settings from here as needed.
#
# Format: INI. Lists are comma-separated, booleans are true/false, an empty
# value means unset, and a _b64 suffix means the value is base64-encoded.
#
# Comments start with # or ; and may follow a value on the same line. That
# means # and ; cannot appear inside a value - which is why the pre-shared
# key, the one setting that can hold arbitrary text, is base64-encoded.
#
# Apply any change with: systemctl restart vpn-router-setup
#
# Every feature is optional. A file with nothing filled in configures nothing
# and leaves the host reachable and unchanged.
[general]
# Platform: none, generic, azure or gcp.
# none - configuration is deferred. Files are installed and nothing on
# this machine is changed. Set a real platform when ready.
# generic - configure, with no platform-specific additions.
platform = none
# How much of the network configuration you supply, and therefore how much is
# read from the system:
# manual - interface names, int_addr and int_gateway_ip are all given
# below. Nothing is detected.
# interfaces - the two interface names are given; int_addr and
# int_gateway_ip are read from them.
# auto - nothing is given. The interfaces are identified from the
# routing table. Convenient, and able to get it wrong.
mode = manual
[interfaces]
# External faces the untrusted network; internal faces the protected one.
# Required in manual and interfaces mode; worked out for you in auto mode.
external =
internal =
[wan]
# This router's fully-qualified domain name.
local_fqdn =
# Source of the local IKE identity: fqdn | public_ip | internal_ip
# fqdn - use local_fqdn (must match what the peer expects)
# public_ip - resolve local_fqdn at boot and use the address
# internal_ip - use the address on the internal interface
local_id_mode = fqdn
[local]
# Local subnets advertised into the tunnel, comma-separated.
cidrs =
# This host's own address on the internal network, and the next hop on that
# side for the subnets above. Both are required in manual mode and read from
# the internal interface in the other two.
int_addr =
int_gateway_ip =
[remote]
# Remote gateway address(es) or FQDN, comma-separated.
addrs =
# Remote IKE identity, without a leading '@'.
id =
# Remote subnets reachable through the tunnel, comma-separated.
cidrs =
# Pre-shared key, base64-encoded: printf %s "$PSK" | base64 -w0
psk_b64 =
# Path to a file containing the raw PSK. Takes precedence over psk_b64.
psk_file =
[p2s]
# Road-warrior access.
enabled = false
# Address pool handed to road-warrior clients.
address_pool =
# CN for the CA created when /etc/vpn-router/pki is empty.
ca_name = VPN Router CA
[wireguard]
enabled = false
# Address and prefix length for wg0, for example 192.168.200.1/24.
address =
listen_port = 51820
@@ -0,0 +1,36 @@
{
"general": {
"platform": "generic",
"mode": "manual"
},
"interfaces": {
"external": "eth0",
"internal": "eth1"
},
"wan": {
"local_fqdn": "router.example.com",
"local_id_mode": "fqdn"
},
"local": {
"cidrs": ["10.0.0.0/24"],
"int_addr": "10.1.1.4",
"int_gateway_ip": "10.1.1.1"
},
"remote": {
"addrs": ["peer.example.net"],
"id": "peer.example.net",
"cidrs": ["192.168.0.0/24"],
"psk": "change-me",
"psk_file": ""
},
"p2s": {
"enabled": false,
"address_pool": "172.16.0.0/24",
"ca_name": "VPN Router CA"
},
"wireguard": {
"enabled": false,
"address": "192.168.200.1/24",
"listen_port": 51820
}
}
+15 -18
View File
@@ -11,24 +11,21 @@ locals {
repo_url = var.repo_url repo_url = var.repo_url
repo_gpg_key = data.http.repo_gpg_key.response_body repo_gpg_key = data.http.repo_gpg_key.response_body
ubuntu_codename = var.ubuntu_codename ubuntu_codename = var.ubuntu_codename
platform = "azure"
mode = "auto" # Contents of /etc/vpn-router/vpn-router.json.
external_interface = "" config = {
internal_interface = "" general = { platform = "azure", mode = "auto" }
local_id_mode = var.local_id_mode wan = { local_fqdn = var.local_fqdn, local_id_mode = var.local_id_mode }
local_cidrs = var.local_cidrs local = { cidrs = [for c in split(",", var.local_cidrs) : trimspace(c) if trimspace(c) != ""] }
int_addr = "" remote = {
int_gateway_ip = "" addrs = [for a in split(",", var.remote_addrs) : trimspace(a) if trimspace(a) != ""]
remote_addrs = var.remote_addrs id = var.remote_id
remote_id = var.remote_id cidrs = local.remote_cidr_list
remote_cidrs = var.remote_cidrs psk = var.psk
psk_b64 = base64encode(var.psk) }
p2s_enabled = var.p2s_enabled p2s = { enabled = var.p2s_enabled, address_pool = var.p2s_address_pool, ca_name = "VPN Router CA" }
p2s_address_pool = var.p2s_address_pool wireguard = { enabled = var.wireguard_enabled, address = var.wireguard_address, listen_port = var.wireguard_listen_port }
p2s_ca_name = "VPN Router CA" }
wg_enabled = var.wireguard_enabled
wg_address = var.wireguard_address
wg_listen_port = var.wireguard_listen_port
} }
} }
+2 -33
View File
@@ -22,42 +22,11 @@ apt:
write_files: write_files:
# The configuration file. Created here before the package is installed, so # The configuration file. Created here before the package is installed, so
# postinst leaves it alone. # postinst leaves it alone.
- path: /etc/vpn-router/vpn-router.conf - path: /etc/vpn-router/vpn-router.json
permissions: '0600' permissions: '0600'
owner: root:root owner: root:root
content: | content: |
[general] ${jsonencode(config)}
platform = ${platform}
mode = ${mode}
[interfaces]
external = ${external_interface}
internal = ${internal_interface}
[wan]
local_fqdn = ${fqdn}
local_id_mode = ${local_id_mode}
[local]
cidrs = ${local_cidrs}
int_addr = ${int_addr}
int_gateway_ip = ${int_gateway_ip}
[remote]
addrs = ${remote_addrs}
id = ${remote_id}
cidrs = ${remote_cidrs}
psk_b64 = ${psk_b64}
[p2s]
enabled = ${p2s_enabled}
address_pool = ${p2s_address_pool}
ca_name = ${p2s_ca_name}
[wireguard]
enabled = ${wg_enabled}
address = ${wg_address}
listen_port = ${wg_listen_port}
- path: /etc/vpn-router/pki/ca_cert.pem - path: /etc/vpn-router/pki/ca_cert.pem
permissions: '0644' permissions: '0644'
+2 -33
View File
@@ -28,42 +28,11 @@ apt:
write_files: write_files:
# The configuration file. Created here before the package is installed, so # The configuration file. Created here before the package is installed, so
# postinst leaves it alone. # postinst leaves it alone.
- path: /etc/vpn-router/vpn-router.conf - path: /etc/vpn-router/vpn-router.json
permissions: '0600' permissions: '0600'
owner: root:root owner: root:root
content: | content: |
[general] ${jsonencode(config)}
platform = ${platform}
mode = ${mode}
[interfaces]
external = ${external_interface}
internal = ${internal_interface}
[wan]
local_fqdn = ${fqdn}
local_id_mode = ${local_id_mode}
[local]
cidrs = ${local_cidrs}
int_addr = ${int_addr}
int_gateway_ip = ${int_gateway_ip}
[remote]
addrs = ${remote_addrs}
id = ${remote_id}
cidrs = ${remote_cidrs}
psk_b64 = ${psk_b64}
[p2s]
enabled = ${p2s_enabled}
address_pool = ${p2s_address_pool}
ca_name = ${p2s_ca_name}
[wireguard]
enabled = ${wg_enabled}
address = ${wg_address}
listen_port = ${wg_listen_port}
package_update: true package_update: true
+15 -18
View File
@@ -11,24 +11,21 @@ locals {
repo_url = var.repo_url repo_url = var.repo_url
repo_gpg_key = data.http.repo_gpg_key.response_body repo_gpg_key = data.http.repo_gpg_key.response_body
ubuntu_codename = var.ubuntu_codename ubuntu_codename = var.ubuntu_codename
platform = "gcp"
mode = "auto" # Contents of /etc/vpn-router/vpn-router.json.
external_interface = "" config = {
internal_interface = "" general = { platform = "gcp", mode = "auto" }
local_id_mode = var.local_id_mode wan = { local_fqdn = var.local_fqdn, local_id_mode = var.local_id_mode }
local_cidrs = var.local_cidrs local = { cidrs = [for c in split(",", var.local_cidrs) : trimspace(c) if trimspace(c) != ""] }
int_addr = "" remote = {
int_gateway_ip = "" addrs = [for a in split(",", var.remote_addrs) : trimspace(a) if trimspace(a) != ""]
remote_addrs = var.remote_addrs id = var.remote_id
remote_id = var.remote_id cidrs = local.remote_cidr_list
remote_cidrs = var.remote_cidrs psk = var.psk
psk_b64 = base64encode(var.psk) }
p2s_enabled = var.p2s_enabled p2s = { enabled = var.p2s_enabled, address_pool = var.p2s_address_pool, ca_name = "VPN Router CA" }
p2s_address_pool = var.p2s_address_pool wireguard = { enabled = var.wireguard_enabled, address = var.wireguard_address, listen_port = var.wireguard_listen_port }
p2s_ca_name = "VPN Router CA" }
wg_enabled = var.wireguard_enabled
wg_address = var.wireguard_address
wg_listen_port = var.wireguard_listen_port
} }
cloud_init_rendered = local.supply_pki ? templatefile("${path.module}/../cloud-init-with-pki.yaml.tpl", merge(local.cloud_init_vars, { cloud_init_rendered = local.supply_pki ? templatefile("${path.module}/../cloud-init-with-pki.yaml.tpl", merge(local.cloud_init_vars, {
+43 -37
View File
@@ -10,18 +10,19 @@ set -e
DEB="${DEB:-./vpn-router_1.0.0-1_all.deb}" DEB="${DEB:-./vpn-router_1.0.0-1_all.deb}"
# List values are JSON arrays.
PLATFORM="${PLATFORM:-generic}" PLATFORM="${PLATFORM:-generic}"
MODE="${MODE:-manual}" MODE="${MODE:-manual}"
EXTERNAL_INTERFACE="${EXTERNAL_INTERFACE:-eth0}" EXTERNAL_INTERFACE="${EXTERNAL_INTERFACE:-eth0}"
INTERNAL_INTERFACE="${INTERNAL_INTERFACE:-eth1}" INTERNAL_INTERFACE="${INTERNAL_INTERFACE:-eth1}"
LOCAL_FQDN="${LOCAL_FQDN:-router.example.com}" LOCAL_FQDN="${LOCAL_FQDN:-router.example.com}"
LOCAL_ID_MODE="${LOCAL_ID_MODE:-fqdn}" LOCAL_ID_MODE="${LOCAL_ID_MODE:-fqdn}"
LOCAL_CIDRS="${LOCAL_CIDRS:-10.0.0.0/24}" LOCAL_CIDRS="${LOCAL_CIDRS:-[\"10.0.0.0/24\"]}"
INT_ADDR="${INT_ADDR:-10.1.1.4}" INT_ADDR="${INT_ADDR:-10.1.1.4}"
INT_GATEWAY_IP="${INT_GATEWAY_IP:-10.1.1.1}" INT_GATEWAY_IP="${INT_GATEWAY_IP:-10.1.1.1}"
REMOTE_ADDRS="${REMOTE_ADDRS:-peer.example.net}" REMOTE_ADDRS="${REMOTE_ADDRS:-[\"peer.example.net\"]}"
REMOTE_ID="${REMOTE_ID:-peer.example.net}" REMOTE_ID="${REMOTE_ID:-peer.example.net}"
REMOTE_CIDRS="${REMOTE_CIDRS:-192.168.0.0/24}" REMOTE_CIDRS="${REMOTE_CIDRS:-[\"192.168.0.0/24\"]}"
PSK="${PSK:-change-me}" PSK="${PSK:-change-me}"
P2S_ENABLED="${P2S_ENABLED:-false}" P2S_ENABLED="${P2S_ENABLED:-false}"
P2S_ADDRESS_POOL="${P2S_ADDRESS_POOL:-172.16.0.0/24}" P2S_ADDRESS_POOL="${P2S_ADDRESS_POOL:-172.16.0.0/24}"
@@ -30,42 +31,47 @@ WG_ENABLED="${WG_ENABLED:-false}"
WG_ADDRESS="${WG_ADDRESS:-}" WG_ADDRESS="${WG_ADDRESS:-}"
WG_LISTEN_PORT="${WG_LISTEN_PORT:-51820}" WG_LISTEN_PORT="${WG_LISTEN_PORT:-51820}"
# Write the configuration before installing, so postinst leaves it alone. # Write the configuration before installing, so postinst leaves it alone. The
# pre-shared key goes to its own file, so it needs no JSON escaping.
install -d -m 0755 /etc/vpn-router install -d -m 0755 /etc/vpn-router
umask 077 umask 077
cat > /etc/vpn-router/vpn-router.conf <<EOF printf %s "$PSK" > /etc/vpn-router/psk
[general] cat > /etc/vpn-router/vpn-router.json <<EOF
platform = ${PLATFORM} {
mode = ${MODE} "general": {
"platform": "${PLATFORM}",
[interfaces] "mode": "${MODE}"
external = ${EXTERNAL_INTERFACE} },
internal = ${INTERNAL_INTERFACE} "interfaces": {
"external": "${EXTERNAL_INTERFACE}",
[wan] "internal": "${INTERNAL_INTERFACE}"
local_fqdn = ${LOCAL_FQDN} },
local_id_mode = ${LOCAL_ID_MODE} "wan": {
"local_fqdn": "${LOCAL_FQDN}",
[local] "local_id_mode": "${LOCAL_ID_MODE}"
cidrs = ${LOCAL_CIDRS} },
int_addr = ${INT_ADDR} "local": {
int_gateway_ip = ${INT_GATEWAY_IP} "cidrs": ${LOCAL_CIDRS},
"int_addr": "${INT_ADDR}",
[remote] "int_gateway_ip": "${INT_GATEWAY_IP}"
addrs = ${REMOTE_ADDRS} },
id = ${REMOTE_ID} "remote": {
cidrs = ${REMOTE_CIDRS} "addrs": ${REMOTE_ADDRS},
psk_b64 = $(printf %s "$PSK" | base64 -w0) "id": "${REMOTE_ID}",
"cidrs": ${REMOTE_CIDRS},
[p2s] "psk_file": "/etc/vpn-router/psk"
enabled = ${P2S_ENABLED} },
address_pool = ${P2S_ADDRESS_POOL} "p2s": {
ca_name = ${P2S_CA_NAME} "enabled": ${P2S_ENABLED},
"address_pool": "${P2S_ADDRESS_POOL}",
[wireguard] "ca_name": "${P2S_CA_NAME}"
enabled = ${WG_ENABLED} },
address = ${WG_ADDRESS} "wireguard": {
listen_port = ${WG_LISTEN_PORT} "enabled": ${WG_ENABLED},
"address": "${WG_ADDRESS}",
"listen_port": ${WG_LISTEN_PORT}
}
}
EOF EOF
umask 022 umask 022