Defer configuration by default and manage vpn-router.conf with ucf

Add platform = none as the shipped default so installing the package
changes nothing on the machine until a platform is chosen. Add a mode
setting (manual/interfaces/auto) controlling how much of the network
configuration is supplied versus detected from the system. Manage
/etc/vpn-router/vpn-router.conf with ucf instead of writing it once,
so dpkg-reconfigure can safely reapply debconf answers without
clobbering local edits. Extend NAT/forward rules to all local subnets,
not just the first.
This commit is contained in:
2026-08-23 17:15:49 +02:00
parent 083ad9a596
commit 7cb2f1b8dd
14 changed files with 439 additions and 124 deletions
+1
View File
@@ -6,3 +6,4 @@ AGENTS.md
CLAUDE.md CLAUDE.md
__pycache__ __pycache__
.vscode .vscode
tmp
+68 -25
View File
@@ -7,12 +7,16 @@ A Debian package that configures a Linux host or virtual machine as a VPN router
- optional WireGuard endpoint - optional WireGuard endpoint
- routing and NAT for a protected subnet, TCP MSS clamping, and UFW firewall rules - routing and NAT for a protected subnet, TCP MSS clamping, and UFW firewall rules
Nothing in the package is specific to a cloud provider. It is built on two inputs, the Nothing in the package is specific to a cloud provider. It is built on the external and
name of the external NIC and the name of the internal one; the addresses on them are read internal NIC names, and a `mode` setting says how much of the rest you are supplying and
from the system rather than configured again. Nothing is auto-detected, because a router how much is read from the system. Detection happens only in the mode that asks for it, so
that guesses which interface to NAT out of is a router that can guess wrong silently. a router never quietly guesses which interface to NAT out of unless you told it to.
Platform-specific additions live in separate modules and are selected explicitly. Platform-specific additions live in separate modules and are selected explicitly.
Installing the package configures nothing by default. The shipped platform is `none`,
which means the files are laid down and the machine is left alone until you say
otherwise.
## Installing ## Installing
The package can be installed by hand or as a step in automated provisioning. Both are The package can be installed by hand or as a step in automated provisioning. Both are
@@ -27,27 +31,32 @@ systemctl restart vpn-router-setup apply it
### By hand ### By hand
```sh ```sh
apt-get install vpn-router apt-get install vpn-router # answer "none" to the platform question
$EDITOR /etc/vpn-router/vpn-router.conf $EDITOR /etc/vpn-router/vpn-router.conf
systemctl restart vpn-router-setup systemctl restart vpn-router-setup
``` ```
Accepting every default at install time is safe. Installing the package enables IP Answering `none` to the platform question defers everything: no further questions are
forwarding and relaxes `rp_filter` through its own drop-in in `/etc/sysctl.d/`, which a asked and nothing on the machine is configured. The only change an install makes is the
router needs, but it configures no tunnels, adds no firewall rules and does not enable the package's own `/etc/sysctl.d/` drop-in, which enables IP forwarding and relaxes
firewall. Fill the file in when you are ready. `rp_filter` as a router needs. Set `platform` in the file when you are ready.
### Automated ### Automated
Preseed the answers and the router comes up configured, with no follow-up command: Preseed the answers and the router comes up configured, with no follow-up command. Note
that `platform` must be something other than `none`, and `mode` says how much you are
supplying:
```sh ```shell
debconf-set-selections <<'EOF' debconf-set-selections <<'EOF'
vpn-router vpn-router/platform select generic
vpn-router vpn-router/mode select manual
vpn-router vpn-router/external_interface string eth0 vpn-router vpn-router/external_interface string eth0
vpn-router vpn-router/internal_interface string eth1 vpn-router vpn-router/internal_interface string eth1
vpn-router vpn-router/int_addr string 10.1.1.4
vpn-router vpn-router/int_gateway_ip string 10.1.1.1
vpn-router vpn-router/local_fqdn string router.example.com vpn-router vpn-router/local_fqdn string router.example.com
vpn-router vpn-router/local_cidrs string 10.0.0.0/24 vpn-router vpn-router/local_cidrs string 10.0.0.0/24
vpn-router vpn-router/int_gateway_ip string 10.1.1.1
vpn-router vpn-router/remote_addrs string peer.example.net vpn-router vpn-router/remote_addrs string peer.example.net
vpn-router vpn-router/remote_id string peer.example.net vpn-router vpn-router/remote_id string peer.example.net
vpn-router vpn-router/remote_cidrs string 192.168.0.0/24 vpn-router vpn-router/remote_cidrs string 192.168.0.0/24
@@ -56,6 +65,9 @@ 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
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.conf` 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`.
@@ -63,22 +75,24 @@ See [examples/](examples/) for a cloud-init template and a shell installer.
## Configuration ## Configuration
`/etc/vpn-router/vpn-router.conf` is INI, mode 0600, and is created once on first install. `/etc/vpn-router/vpn-router.conf` is INI, mode 0600, and is created on first install. It
The package never rewrites it, so it is safe to edit, keep in version control or template. is managed with `ucf`, so it is yours to edit: a later `dpkg-reconfigure` applies what
A fully commented reference is installed at changed and asks before touching anything you altered by hand. A fully commented
`/usr/share/doc/vpn-router/vpn-router.conf.example`. reference is installed at `/usr/share/doc/vpn-router/vpn-router.conf.example`.
Lists are comma-separated, booleans are `true`/`false`, an empty value means unset, and a Lists are comma-separated, booleans are `true`/`false`, an empty value means unset, and a
`_b64` suffix means the value is base64-encoded. `_b64` suffix means the value is base64-encoded.
| Section | Setting | Meaning | | Section | Setting | Meaning |
|---|---|---| |---|---|---|
| `general` | `platform` | Platform module: `generic`, `azure` or `gcp` | | `general` | `platform` | `none`, `generic`, `azure` or `gcp`. `none` defers configuration entirely |
| `general` | `mode` | How much you supply: `manual`, `interfaces` or `auto` |
| `interfaces` | `external` | Name of the NIC facing the untrusted network | | `interfaces` | `external` | Name of the NIC facing the untrusted network |
| `interfaces` | `internal` | Name of the NIC facing the protected network | | `interfaces` | `internal` | Name of the NIC facing the protected network |
| `wan` | `local_fqdn` | This router's FQDN | | `wan` | `local_fqdn` | This router's FQDN |
| `wan` | `local_id_mode` | IKE identity source: `fqdn`, `public_ip` or `internal_ip` | | `wan` | `local_id_mode` | IKE identity source: `fqdn`, `public_ip` or `internal_ip` |
| `local` | `cidrs` | Local subnets advertised into the tunnel | | `local` | `cidrs` | Local subnets advertised into the tunnel |
| `local` | `int_addr` | This host's address on the internal network |
| `local` | `int_gateway_ip` | Next hop on the internal side | | `local` | `int_gateway_ip` | Next hop on the internal side |
| `remote` | `addrs` | Remote gateway address(es) or FQDN | | `remote` | `addrs` | Remote gateway address(es) or FQDN |
| `remote` | `id` | Remote IKE identity, without a leading `@` | | `remote` | `id` | Remote IKE identity, without a leading `@` |
@@ -105,10 +119,26 @@ 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.
The two interface names are the base the rest is built on. The addresses strongSwan binds ### What `mode` decides
on come from the external NIC, and the address used for `local_id_mode = internal_ip`
comes from the internal one, so neither is configured twice and neither can drift out of `mode` states how much of the network configuration you are supplying, and therefore how
step with the host. If a named interface does not exist the service stops and says so. much the package reads from the system:
| `mode` | You provide | The package works out |
|---|---|---|
| `manual` | both interface names, `int_addr`, `int_gateway_ip` | nothing |
| `interfaces` | both interface names | `int_addr` and `int_gateway_ip`, from those interfaces |
| `auto` | nothing | the interface names too, from the routing table |
`manual` is the only mode in which no detection code runs at all. `auto` is the
convenient one and can pick the wrong interface, which is a trade you make deliberately
by choosing it rather than something the package does behind your back.
The addresses strongSwan binds on always come from the external NIC, in every mode, so
they are never configured twice and cannot drift out of step with the host. If a named
interface does not exist the service stops and says so. In `interfaces` and `auto` mode
the internal gateway is taken from the routing table, falling back to the first host
address of the connected subnet with a warning when the routing table has nothing to say.
## Applying changes ## Applying changes
@@ -116,20 +146,28 @@ step with the host. If a named interface does not exist the service stops and sa
systemctl restart vpn-router-setup systemctl restart vpn-router-setup
``` ```
The service also runs at boot. It regenerates the configuration files, reads the current The service also runs at boot. With `platform = none` it does nothing and says so.
addresses of the two interfaces, applies routes, generates WireGuard keys and injects Otherwise it regenerates the configuration files, resolves the interfaces and their
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
feature is configured, reloading or restarting them only when their inputs changed, and feature is configured, reloading or restarting them only when their inputs changed, and
restarts `systemd-resolved` only when the road-warrior DNS drop-in changed. Turning a restarts `systemd-resolved` only when the road-warrior DNS drop-in changed. Turning a
feature off in the configuration stops its service and removes its files. Every step is feature off in the configuration stops its service and removes its files. Every step is
idempotent, so restarting with no changes does nothing. idempotent, so restarting with no changes does nothing.
To regenerate the configuration files without touching the running system: To regenerate the configuration files and PKI without starting, stopping or reloading
anything:
```sh ```sh
/usr/lib/vpn-router/configure /usr/lib/vpn-router/configure
``` ```
This does still write to the system: it renders into `/etc/swanctl/`,
`/etc/systemd/resolved.conf.d/` and `/etc/wireguard/`, and if road-warrior access is
enabled with an empty PKI directory it creates the certificate authority and adds it
to the host trust store. What it leaves alone is routes, firewall rules and services -
that is what `systemctl restart vpn-router-setup` adds.
## Certificates ## Certificates
`/etc/vpn-router/pki` is the PKI directory, laid out the way `simple-ca` expects: `/etc/vpn-router/pki` is the PKI directory, laid out the way `simple-ca` expects:
@@ -169,6 +207,10 @@ Platform-specific values do not belong in the core, so they are Python modules i
`general.platform`. The platform is never probed: the caller already knows where it is `general.platform`. The platform is never probed: the caller already knows where it is
deploying. deploying.
`none` is not a module. It is the shipped default and means "configure nothing at all",
which is how a deferred install is expressed. `generic` is the module to choose when the
router should be configured but the platform adds nothing.
The shipped modules currently add nothing. Road-warrior clients are given the router The shipped modules currently add nothing. Road-warrior clients are given the router
itself as their DNS server and the router forwards through its own `systemd-resolved`, so itself as their DNS server and the router forwards through its own `systemd-resolved`, so
no provider resolver has to be advertised or routed through the tunnel on any platform. no provider resolver has to be advertised or routed through the tunnel on any platform.
@@ -206,7 +248,8 @@ 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. Key material is never deleted: `/etc/ufw/before.rules` to its original content, and hands `vpn-router.conf` back to `ucf`
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.
+35 -3
View File
@@ -2,13 +2,45 @@
set -e set -e
. /usr/share/debconf/confmodule . /usr/share/debconf/confmodule
# Every question has a default, so a non-interactive install with nothing
# preseeded completes without prompting. The defaults are platform "none" and
# mode "manual", which together mean: install the files, configure nothing.
db_input high vpn-router/platform || true db_input high vpn-router/platform || true
db_input high vpn-router/external_interface || true db_go || true
db_input high vpn-router/internal_interface || true
db_get vpn-router/platform
if [ "$RET" = "none" ]; then
# Configuration is deferred. Asking anything else would collect answers
# that nothing is going to apply.
exit 0
fi
db_input high vpn-router/mode || true
db_go || true
# The mode says how much the operator supplies, so it decides which interface
# and address questions are worth asking.
db_get vpn-router/mode
case "$RET" in
manual)
db_input high vpn-router/external_interface || true
db_input high vpn-router/internal_interface || true
db_input high vpn-router/int_addr || true
db_input high vpn-router/int_gateway_ip || true
;;
interfaces)
db_input high vpn-router/external_interface || true
db_input high vpn-router/internal_interface || true
;;
auto)
;;
esac
db_go || true
db_input high vpn-router/local_fqdn || true db_input high vpn-router/local_fqdn || true
db_input high vpn-router/local_id_mode || true db_input high vpn-router/local_id_mode || true
db_input high vpn-router/local_cidrs || true db_input high vpn-router/local_cidrs || true
db_input high vpn-router/int_gateway_ip || true
db_input high vpn-router/remote_addrs || true db_input high vpn-router/remote_addrs || true
db_input high vpn-router/remote_id || true db_input high vpn-router/remote_id || true
db_input high vpn-router/remote_cidrs || true db_input high vpn-router/remote_cidrs || true
+1
View File
@@ -16,6 +16,7 @@ Depends: ${misc:Depends},
wireguard-tools, wireguard-tools,
ufw, ufw,
debconf, debconf,
ucf,
openssl, openssl,
python3, python3,
python3-jinja2, python3-jinja2,
+1 -1
View File
@@ -3,7 +3,7 @@ src/usr/lib/vpn-router/vpnrouter.py usr/lib/vpn-router/
src/usr/lib/vpn-router/simple-ca usr/lib/vpn-router/ src/usr/lib/vpn-router/simple-ca usr/lib/vpn-router/
src/usr/lib/vpn-router/configure usr/lib/vpn-router/ src/usr/lib/vpn-router/configure usr/lib/vpn-router/
src/usr/lib/vpn-router/setup usr/lib/vpn-router/ src/usr/lib/vpn-router/setup usr/lib/vpn-router/
src/usr/lib/vpn-router/seed-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.conf.example usr/share/doc/vpn-router/
+17 -4
View File
@@ -6,6 +6,8 @@ case "$1" in
configure) configure)
# --- Read the installer's answers --- # --- Read the installer's answers ---
db_get vpn-router/platform; VPN_ROUTER_PLATFORM="$RET" db_get vpn-router/platform; VPN_ROUTER_PLATFORM="$RET"
db_get vpn-router/mode; VPN_ROUTER_MODE="$RET"
db_get vpn-router/int_addr; VPN_ROUTER_INT_ADDR="$RET"
db_get vpn-router/external_interface; VPN_ROUTER_EXTERNAL_INTERFACE="$RET" db_get vpn-router/external_interface; VPN_ROUTER_EXTERNAL_INTERFACE="$RET"
db_get vpn-router/internal_interface; VPN_ROUTER_INTERNAL_INTERFACE="$RET" db_get vpn-router/internal_interface; VPN_ROUTER_INTERNAL_INTERFACE="$RET"
db_get vpn-router/local_fqdn; VPN_ROUTER_LOCAL_FQDN="$RET" db_get vpn-router/local_fqdn; VPN_ROUTER_LOCAL_FQDN="$RET"
@@ -23,7 +25,8 @@ case "$1" in
db_get vpn-router/wg_address; VPN_ROUTER_WG_ADDRESS="$RET" db_get vpn-router/wg_address; VPN_ROUTER_WG_ADDRESS="$RET"
db_get vpn-router/wg_listen_port; VPN_ROUTER_WG_LISTEN_PORT="$RET" db_get vpn-router/wg_listen_port; VPN_ROUTER_WG_LISTEN_PORT="$RET"
export VPN_ROUTER_PLATFORM VPN_ROUTER_EXTERNAL_INTERFACE \ export VPN_ROUTER_PLATFORM VPN_ROUTER_MODE VPN_ROUTER_INT_ADDR \
VPN_ROUTER_EXTERNAL_INTERFACE \
VPN_ROUTER_INTERNAL_INTERFACE VPN_ROUTER_LOCAL_FQDN \ VPN_ROUTER_INTERNAL_INTERFACE VPN_ROUTER_LOCAL_FQDN \
VPN_ROUTER_LOCAL_ID_MODE VPN_ROUTER_LOCAL_CIDRS \ VPN_ROUTER_LOCAL_ID_MODE VPN_ROUTER_LOCAL_CIDRS \
VPN_ROUTER_INT_GATEWAY_IP \ VPN_ROUTER_INT_GATEWAY_IP \
@@ -33,10 +36,20 @@ case "$1" in
VPN_ROUTER_P2S_CA_NAME VPN_ROUTER_WG_ENABLED \ VPN_ROUTER_P2S_CA_NAME VPN_ROUTER_WG_ENABLED \
VPN_ROUTER_WG_ADDRESS VPN_ROUTER_WG_LISTEN_PORT VPN_ROUTER_WG_ADDRESS VPN_ROUTER_WG_LISTEN_PORT
# --- Create the configuration file, only if it does not exist --- # --- Hand a candidate configuration to ucf ---
/usr/lib/vpn-router/seed-config # ucf compares it against the file in /etc and decides what to do about
# local changes, prompting through debconf only for a real conflict.
# This is what makes dpkg-reconfigure apply without destroying edits.
CANDIDATE="$(mktemp)"
/usr/lib/vpn-router/generate-config "$CANDIDATE"
ucf --three-way --debconf-ok "$CANDIDATE" /etc/vpn-router/vpn-router.conf
ucfr vpn-router /etc/vpn-router/vpn-router.conf
chmod 0600 /etc/vpn-router/vpn-router.conf
rm -f "$CANDIDATE"
# The key now lives in the configuration file; do not keep a copy. # The key now lives in the configuration file; do not keep a copy. An
# empty answer means "leave alone" next time, so clearing it here does
# not blank the key on the next dpkg-reconfigure.
db_set vpn-router/psk "" db_set vpn-router/psk ""
# Apply the sysctl drop-in shipped by this package so it takes effect # Apply the sysctl drop-in shipped by this package so it takes effect
+9
View File
@@ -34,6 +34,15 @@ case "$1" in
purge) purge)
strip_ufw_blocks strip_ufw_blocks
# Let ucf forget the file before it is removed, or a reinstall finds a
# stale hash and declines to lay the file down again.
if command -v ucf >/dev/null 2>&1; then
ucf --purge /etc/vpn-router/vpn-router.conf
fi
if command -v ucfr >/dev/null 2>&1; then
ucfr --purge vpn-router /etc/vpn-router/vpn-router.conf
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 \
+10 -2
View File
@@ -9,8 +9,16 @@ case "$1" in
if [ -d /run/systemd/system ]; then if [ -d /run/systemd/system ]; then
swanctl --terminate --ike remote-site >/dev/null 2>&1 || true swanctl --terminate --ike remote-site >/dev/null 2>&1 || true
swanctl --terminate --ike road-warrior >/dev/null 2>&1 || true swanctl --terminate --ike road-warrior >/dev/null 2>&1 || true
# This package enabled wg-quick@wg0, so it takes it down again. fi
systemctl disable --now wg-quick@wg0 >/dev/null 2>&1 || true
# This package enabled wg-quick@wg0, so it takes it down again. Use the
# debhelper wrappers rather than systemctl, so the script behaves on a
# machine without systemd.
if command -v deb-systemd-invoke >/dev/null 2>&1; then
deb-systemd-invoke stop wg-quick@wg0.service >/dev/null 2>&1 || true
fi
if command -v deb-systemd-helper >/dev/null 2>&1; then
deb-systemd-helper disable wg-quick@wg0.service >/dev/null 2>&1 || true
fi fi
;; ;;
esac esac
+53 -24
View File
@@ -1,34 +1,62 @@
Template: vpn-router/platform Template: vpn-router/platform
Type: select Type: select
Choices: generic, azure, gcp Choices: none, generic, azure, gcp
Default: generic Default: none
Description: Platform module Description: Platform:
Platform-specific additions to load. The router works without any of them; Which platform this router runs on. The choice loads platform-specific
a module only adds what makes sense on its platform, such as the platform additions, such as the provider DNS resolver for road-warrior clients.
DNS resolver for road-warrior clients. .
none defers configuration entirely: the package installs its files and
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
/etc/vpn-router/vpn-router.conf when you are ready.
.
generic configures the router with no platform-specific additions.
Template: vpn-router/mode
Type: select
Choices: manual, interfaces, auto
Default: manual
Description: Network configuration source:
How much of the network configuration you are supplying, and therefore how
much the package works out for itself:
.
manual - you give the interface names, the internal address and the internal
gateway. Nothing is detected.
.
interfaces - you give the two interface names. The internal address and
gateway are read from them.
.
auto - you give nothing. The interfaces are identified from the routing
table and their addresses read from the system. Convenient, but it can pick
the wrong interface and configure the machine incorrectly.
Template: vpn-router/external_interface Template: vpn-router/external_interface
Type: string Type: string
Default: Default:
Description: External network interface Description: External network interface:
Name of the interface facing the untrusted network, for example eth0 or Name of the interface facing the untrusted network, for example eth0 or
ens4. The addresses strongSwan binds on are read from it, so they are never ens4. The addresses strongSwan binds on are read from it, so they are never
configured separately. configured separately.
.
Leave empty to configure the router later by editing
/etc/vpn-router/vpn-router.conf.
Template: vpn-router/internal_interface Template: vpn-router/internal_interface
Type: string Type: string
Default: Default:
Description: Internal network interface Description: Internal network interface:
Name of the interface facing the protected network, for example eth1 or Name of the interface facing the protected network, for example eth1 or
ens5. Routes for the local subnets are applied to it. ens5. Routes for the local subnets are applied to it.
Template: vpn-router/int_addr
Type: string
Default:
Description: Internal interface address:
This host's own address on the internal network. Asked only in manual mode;
in the other modes it is read from the internal interface.
Template: vpn-router/local_fqdn Template: vpn-router/local_fqdn
Type: string Type: string
Default: Default:
Description: Local router FQDN Description: Local router FQDN:
Fully-qualified domain name of this router (for example Fully-qualified domain name of this router (for example
router.example.com). Used as the road-warrior server identity and router.example.com). Used as the road-warrior server identity and
certificate common name. certificate common name.
@@ -37,7 +65,7 @@ Template: vpn-router/local_id_mode
Type: select Type: select
Choices: fqdn, public_ip, internal_ip Choices: fqdn, public_ip, internal_ip
Default: fqdn Default: fqdn
Description: IKE local identity mode Description: IKE local identity mode:
How to derive the IKE identity advertised to the remote site: How to derive the IKE identity advertised to the remote site:
. .
fqdn - use the FQDN, which must match what the peer expects. fqdn - use the FQDN, which must match what the peer expects.
@@ -49,40 +77,41 @@ Description: IKE local identity mode
Template: vpn-router/local_cidrs Template: vpn-router/local_cidrs
Type: string Type: string
Default: Default:
Description: Local subnet CIDR(s) Description: Local subnet CIDR(s):
Comma-separated list of local subnet CIDRs to advertise into the Comma-separated list of local subnet CIDRs to advertise into the
site-to-site tunnel (for example 10.0.0.0/24 or 10.0.0.0/24,10.0.1.0/24). site-to-site tunnel (for example 10.0.0.0/24 or 10.0.0.0/24,10.0.1.0/24).
Template: vpn-router/int_gateway_ip Template: vpn-router/int_gateway_ip
Type: string Type: string
Default: Default:
Description: Internal network gateway IP Description: Internal network gateway address:
Address of the next-hop gateway on the internal side, used to route the Address of the next-hop gateway on the internal side, used to route the
local subnets listed above. local subnets listed above. Asked only in manual mode; in the other modes it
is derived from the internal interface.
Template: vpn-router/remote_addrs Template: vpn-router/remote_addrs
Type: string Type: string
Default: Default:
Description: Remote site WAN IP address(es) Description: Remote site WAN IP address(es):
Comma-separated list of remote gateway addresses or FQDNs for the Comma-separated list of remote gateway addresses or FQDNs for the
site-to-site IPSec tunnel. site-to-site IPSec tunnel.
Template: vpn-router/remote_id Template: vpn-router/remote_id
Type: string Type: string
Default: Default:
Description: Remote site IKE identity Description: Remote site IKE identity:
IKE identity of the remote peer, without a leading @. IKE identity of the remote peer, without a leading @.
Template: vpn-router/remote_cidrs Template: vpn-router/remote_cidrs
Type: string Type: string
Default: Default:
Description: Remote subnet CIDR(s) Description: Remote subnet CIDR(s):
Comma-separated list of remote subnet CIDRs reachable through the Comma-separated list of remote subnet CIDRs reachable through the
site-to-site tunnel (for example 192.168.0.0/24). site-to-site tunnel (for example 192.168.0.0/24).
Template: vpn-router/psk 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 base64-encoded in the configuration
file and cleared from the debconf database after installation. file and cleared from the debconf database after installation.
@@ -98,13 +127,13 @@ Description: Enable road-warrior (P2S) access?
Template: vpn-router/p2s_address_pool Template: vpn-router/p2s_address_pool
Type: string Type: string
Default: Default:
Description: Road-warrior address pool Description: Road-warrior address pool:
CIDR block assigned to road-warrior clients (for example 172.16.0.0/24). CIDR block assigned to road-warrior clients (for example 172.16.0.0/24).
Template: vpn-router/p2s_ca_name Template: vpn-router/p2s_ca_name
Type: string Type: string
Default: VPN Router CA Default: VPN Router CA
Description: Road-warrior CA name Description: Road-warrior CA name:
Common name for the certificate authority created in /etc/vpn-router/pki Common name for the certificate authority created in /etc/vpn-router/pki
when that directory is empty. Ignored when certificates are supplied. when that directory is empty. Ignored when certificates are supplied.
@@ -119,12 +148,12 @@ Description: Enable WireGuard?
Template: vpn-router/wg_address Template: vpn-router/wg_address
Type: string Type: string
Default: Default:
Description: WireGuard interface address Description: WireGuard interface address:
Address and prefix length for the wg0 interface (for example Address and prefix length for the wg0 interface (for example
192.168.200.1/24). 192.168.200.1/24).
Template: vpn-router/wg_listen_port Template: vpn-router/wg_listen_port
Type: string Type: string
Default: 51820 Default: 51820
Description: WireGuard listen port Description: WireGuard listen port:
UDP port that WireGuard listens on. UDP port that WireGuard listens on.
+5 -2
View File
@@ -1,8 +1,11 @@
#!/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.conf.
Run directly to regenerate the configuration files without touching the Run directly to regenerate them without starting, stopping or reloading
running system. 'setup' calls the same code before applying the rest. anything. This still writes to /etc/swanctl, /etc/systemd/resolved.conf.d and
/etc/wireguard, and may create the certificate authority and add it to the host
trust store; what it leaves alone is routes, firewall rules and services.
'setup' calls the same code before applying those.
""" """
import os import os
@@ -1,9 +1,23 @@
#!/usr/bin/python3 #!/usr/bin/python3
"""Create /etc/vpn-router/vpn-router.conf from the installer's answers. """Write a candidate vpn-router.conf to the path given on the command line.
Runs once, from postinst, and only when the file does not exist. The Called from postinst, which hands the result to ucf(1). ucf compares it with
configuration file belongs to the operator from that point on and is never /etc/vpn-router/vpn-router.conf and decides what to do about local changes, so
rewritten by the package. this script never writes to /etc and never has to worry about destroying an
administrator's edits.
The candidate is built in three layers, each overriding the one before:
1. the shipped defaults
2. the current contents of /etc/vpn-router/vpn-router.conf, if it exists
3. any debconf answer that was actually given
Layer 2 is what makes reconfiguration safe: a setting the administrator edited
by hand, and that debconf has nothing to say about, is carried through
unchanged, so ucf only sees a diff where something really changed. An empty
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
dpkg-reconfigure.
Answers arrive as VPN_ROUTER_* environment variables. The pre-shared key is Answers arrive as VPN_ROUTER_* environment variables. The pre-shared key is
stored base64-encoded so that arbitrary characters cannot collide with INI stored base64-encoded so that arbitrary characters cannot collide with INI
@@ -21,6 +35,8 @@ import vpnrouter
# environment variable -> (section, option) # environment variable -> (section, option)
ANSWERS = { ANSWERS = {
'VPN_ROUTER_PLATFORM': ('general', 'platform'), 'VPN_ROUTER_PLATFORM': ('general', 'platform'),
'VPN_ROUTER_MODE': ('general', 'mode'),
'VPN_ROUTER_INT_ADDR': ('local', 'int_addr'),
'VPN_ROUTER_EXTERNAL_INTERFACE': ('interfaces', 'external'), 'VPN_ROUTER_EXTERNAL_INTERFACE': ('interfaces', 'external'),
'VPN_ROUTER_INTERNAL_INTERFACE': ('interfaces', 'internal'), 'VPN_ROUTER_INTERNAL_INTERFACE': ('interfaces', 'internal'),
'VPN_ROUTER_LOCAL_FQDN': ('wan', 'local_fqdn'), 'VPN_ROUTER_LOCAL_FQDN': ('wan', 'local_fqdn'),
@@ -40,31 +56,31 @@ ANSWERS = {
def main(): def main():
path = vpnrouter.CONFIG_FILE if len(sys.argv) != 2:
if path.exists(): print('usage: generate-config OUTPUT', file=sys.stderr)
return 0 return 2
output = sys.argv[1]
parser = vpnrouter.default_parser() parser = vpnrouter.default_parser()
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():
value = os.environ.get(variable) if os.environ.get(variable):
if value is not None and value != '': parser[section][option] = os.environ[variable]
parser[section][option] = value
psk = os.environ.get('VPN_ROUTER_PSK', '') psk = os.environ.get('VPN_ROUTER_PSK', '')
if psk: if psk:
parser['remote']['psk_b64'] = base64.b64encode( parser['remote']['psk_b64'] = base64.b64encode(
psk.encode('utf-8')).decode('ascii') psk.encode('utf-8')).decode('ascii')
path.parent.mkdir(parents=True, exist_ok=True) with open(output, 'w', encoding='utf-8') as handle:
with open(path, 'w', encoding='utf-8') as handle:
parser.write(handle) parser.write(handle)
os.chmod(path, 0o600) os.chmod(output, 0o600)
os.chown(path, 0, 0)
print(f'vpn-router: wrote {path}')
print('vpn-router: edit it and run "systemctl restart vpn-router-setup" '
'to apply changes')
return 0 return 0
+23 -16
View File
@@ -150,14 +150,13 @@ def filter_block(ctx):
lines += ['# P2S DNS RULES END'] lines += ['# P2S DNS RULES END']
if vpnrouter.routing_ready(ctx): if vpnrouter.routing_ready(ctx):
subnet = ctx['local_subnet'] lines += ['', '# ROUTER FORWARD RULES START']
lines += [ for subnet in ctx['local_cidr_list']:
'', lines += [
'# ROUTER FORWARD RULES START', f'-A ufw-before-forward -s {subnet} -o {ctx["wan_iface"]} -j ACCEPT',
f'-A ufw-before-forward -s {subnet} -o {ctx["wan_iface"]} -j ACCEPT', f'-A ufw-before-forward -d {subnet} -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT',
f'-A ufw-before-forward -d {subnet} -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT', ]
'# ROUTER FORWARD RULES END', lines += ['# ROUTER FORWARD RULES END']
]
return ''.join(line + '\n' for line in lines) return ''.join(line + '\n' for line in lines)
@@ -166,7 +165,8 @@ def nat_and_mangle_block(ctx):
lines = [] lines = []
if vpnrouter.routing_ready(ctx): if vpnrouter.routing_ready(ctx):
subnet = ctx['local_subnet'] subnets = ctx['local_cidr_list']
remote = [c.strip() for c in ctx['remote_cidrs'].split(',') if c.strip()]
lines += [ lines += [
'', '',
'# ROUTER NAT RULES START', '# ROUTER NAT RULES START',
@@ -174,13 +174,15 @@ def nat_and_mangle_block(ctx):
':POSTROUTING ACCEPT [0:0]', ':POSTROUTING ACCEPT [0:0]',
'-F POSTROUTING', '-F POSTROUTING',
] ]
for cidr in [c.strip() for c in ctx['remote_cidrs'].split(',') if c.strip()]: # Every RETURN precedes every MASQUERADE, so tunnel-bound traffic
lines.append(f'-A POSTROUTING -s {subnet} -d {cidr} -j RETURN') # escapes NAT whichever local subnet it came from.
lines += [ for subnet in subnets:
f'-A POSTROUTING -s {subnet} -o {ctx["wan_iface"]} -j MASQUERADE', for cidr in remote:
'COMMIT', lines.append(f'-A POSTROUTING -s {subnet} -d {cidr} -j RETURN')
'# ROUTER NAT RULES END', for subnet in subnets:
] lines.append(
f'-A POSTROUTING -s {subnet} -o {ctx["wan_iface"]} -j MASQUERADE')
lines += ['COMMIT', '# ROUTER NAT RULES END']
lines += [ lines += [
'', '',
@@ -301,6 +303,11 @@ def main():
cfg = vpnrouter.load_config() cfg = vpnrouter.load_config()
ctx, changed = vpnrouter.configure(cfg) ctx, changed = vpnrouter.configure(cfg)
if ctx is None:
print('vpn-router: platform is none, configuration deferred; '
'set general.platform in /etc/vpn-router/vpn-router.conf')
return 0
if vpnrouter.routing_ready(ctx): if vpnrouter.routing_ready(ctx):
apply_routes(ctx) apply_routes(ctx)
@@ -41,10 +41,10 @@ TRUST_ANCHOR = pathlib.Path('/usr/local/share/ca-certificates/vpn-router-ca.cr
# section -> option -> default # section -> option -> default
SCHEMA = { SCHEMA = {
'general': {'platform': 'generic'}, '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_gateway_ip': ''}, 'local': {'cidrs': '', 'int_addr': '', 'int_gateway_ip': ''},
'remote': {'addrs': '', 'id': '', 'cidrs': '', 'psk_b64': '', 'psk_file': ''}, 'remote': {'addrs': '', 'id': '', 'cidrs': '', 'psk_b64': '', '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'},
@@ -52,6 +52,17 @@ SCHEMA = {
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.
#: manual - names and addresses given; nothing is detected
#: interfaces - names given; addresses and gateway derived from them
#: auto - nothing given; the interfaces are worked out too, accepting
#: that the result may be wrong
MODES = ('manual', 'interfaces', 'auto')
#: platform = none means configuration is deferred: files are installed and
#: nothing else happens until the operator says otherwise.
PLATFORM_NONE = 'none'
class ConfigError(Exception): class ConfigError(Exception):
"""A setting is present but malformed.""" """A setting is present but malformed."""
@@ -92,8 +103,13 @@ def default_parser():
return parser return parser
def load_config(path=CONFIG_FILE): def load_config(path=None):
"""Read the configuration, filling in defaults for anything absent.""" """Read the configuration, filling in defaults for anything absent.
The path is resolved when called, not when this function is defined, so
CONFIG_FILE stays overridable.
"""
path = pathlib.Path(path) if path else CONFIG_FILE
parser = default_parser() parser = default_parser()
if path.exists(): if path.exists():
try: try:
@@ -137,14 +153,35 @@ def _check_address(value, name):
def validate(cfg): def validate(cfg):
"""Reject values that are present but malformed. Absent is not an error.""" """Reject values that are present but malformed. Absent is not an error.
The exception is general.mode, which states what the operator promised to
supply, so the values that mode requires are checked for presence too.
"""
platform = _get(cfg, 'general', 'platform') platform = _get(cfg, 'general', 'platform')
installed = vpnrouter_platforms.available() installed = [PLATFORM_NONE] + vpnrouter_platforms.available()
if platform and platform not in installed: if platform and platform not in installed:
raise ConfigError( raise ConfigError(
f'general.platform: {platform!r} is not installed, expected one of ' f'general.platform: {platform!r} is not installed, expected one of '
f'{", ".join(installed)}') f'{", ".join(installed)}')
mode = _get(cfg, 'general', 'mode')
if mode and mode not in MODES:
raise ConfigError(
f'general.mode: {mode!r} is not one of {", ".join(MODES)}')
if not deferred(cfg):
required = {
'manual': (('interfaces', 'external'), ('interfaces', 'internal'),
('local', 'int_addr'), ('local', 'int_gateway_ip')),
'interfaces': (('interfaces', 'external'), ('interfaces', 'internal')),
'auto': (),
}[mode or 'manual']
for section, option in required:
if not _get(cfg, section, option):
raise ConfigError(
f'{section}.{option} is required in mode {mode or "manual"}')
id_mode = _get(cfg, 'wan', 'local_id_mode') id_mode = _get(cfg, 'wan', 'local_id_mode')
if id_mode and id_mode not in ID_MODES: if id_mode and id_mode not in ID_MODES:
raise ConfigError( raise ConfigError(
@@ -153,13 +190,16 @@ def validate(cfg):
_check_networks(_get(cfg, 'local', 'cidrs'), 'local.cidrs') _check_networks(_get(cfg, 'local', 'cidrs'), 'local.cidrs')
_check_networks(_get(cfg, 'remote', 'cidrs'), 'remote.cidrs') _check_networks(_get(cfg, 'remote', 'cidrs'), 'remote.cidrs')
_check_networks(_get(cfg, 'p2s', 'address_pool'), 'p2s.address_pool') _check_networks(_get(cfg, 'p2s', 'address_pool'), 'p2s.address_pool')
_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')
for option in ('external', 'internal'): # In auto mode the names are worked out later, so there is nothing to check.
name = _get(cfg, 'interfaces', option) if mode != 'auto':
if name and not (pathlib.Path('/sys/class/net') / name).exists(): for option in ('external', 'internal'):
raise ConfigError( name = _get(cfg, 'interfaces', option)
f'interfaces.{option}: interface {name!r} does not exist') if name and not (pathlib.Path('/sys/class/net') / name).exists():
raise ConfigError(
f'interfaces.{option}: interface {name!r} does not exist')
wg_address = _get(cfg, 'wireguard', 'address') wg_address = _get(cfg, 'wireguard', 'address')
if wg_address: if wg_address:
@@ -199,6 +239,9 @@ def resolve_psk(cfg):
def platform_module(cfg): def platform_module(cfg):
"""Import the configured platform module, falling back to the default.""" """Import the configured platform module, falling back to the default."""
name = _get(cfg, 'general', 'platform') or vpnrouter_platforms.DEFAULT name = _get(cfg, 'general', 'platform') or vpnrouter_platforms.DEFAULT
if name == PLATFORM_NONE:
# Not normally reached: configure() returns before this when deferred.
name = vpnrouter_platforms.DEFAULT
try: try:
return vpnrouter_platforms.load(name) return vpnrouter_platforms.load(name)
except ModuleNotFoundError: except ModuleNotFoundError:
@@ -216,6 +259,73 @@ def _join(*groups):
return ', '.join(items) return ', '.join(items)
def deferred(cfg):
"""True when platform is none, meaning nothing is to be configured yet."""
return (_get(cfg, 'general', 'platform') or PLATFORM_NONE) == PLATFORM_NONE
def _ip_route(*args):
result = subprocess.run(['ip', *args], capture_output=True, text=True)
return result.stdout if result.returncode == 0 else ''
def detect_interfaces():
"""Work out the external and internal interface names.
Only reached in auto mode, where the operator has accepted that a wrong
guess is possible. The external interface is whichever one carries the
default route; the internal one is the only other addressed interface.
"""
tokens = _ip_route('route', 'get', '1.1.1.1').split()
external = ''
for index, token in enumerate(tokens):
if token == 'dev' and index + 1 < len(tokens):
external = tokens[index + 1]
break
if not external:
raise ConfigError('auto mode: cannot determine the external interface')
candidates = [name for name in _addressed_interfaces() if name != external]
if len(candidates) != 1:
raise ConfigError(
f'auto mode: expected one internal interface, found {len(candidates)}; '
'use mode = interfaces and name them')
return external, candidates[0]
def _addressed_interfaces():
"""Interface names holding at least one IPv4 address, excluding loopback."""
names = []
for line in _ip_route('-o', '-4', 'addr', 'show').splitlines():
fields = line.split()
if len(fields) >= 4 and fields[1] != 'lo' and fields[1] not in names:
names.append(fields[1])
return names
def detect_gateway(iface, address):
"""Derive the next hop on the internal side.
The routing table is the honest source. When it has nothing to say, fall
back to the first host address of the connected subnet, which is a
convention rather than a fact - so say so.
"""
for line in _ip_route('-o', 'route', 'show', 'dev', iface).splitlines():
fields = line.split()
if 'via' in fields:
return fields[fields.index('via') + 1]
for line in _ip_route('-o', '-4', 'addr', 'show', 'dev', iface).splitlines():
fields = line.split()
if len(fields) >= 4 and fields[3].split('/')[0] == address:
network = ipaddress.ip_interface(fields[3]).network
gateway = str(next(network.hosts()))
warn(f'{iface}: no gateway in the routing table, assuming {gateway} '
f'as the first host of {network}')
return gateway
return ''
def interface_addresses(name, setting): def interface_addresses(name, setting):
"""IPv4 addresses configured on an interface. """IPv4 addresses configured on an interface.
@@ -244,20 +354,42 @@ def interface_addresses(name, setting):
return addresses return addresses
def resolve_interfaces(cfg):
"""Return (ext_iface, int_iface, local_addrs, int_addr, int_gateway_ip).
What is read from the system and what is taken verbatim depends entirely on
general.mode. In manual mode nothing below touches the system at all.
"""
mode = _get(cfg, 'general', 'mode') or 'manual'
ext_iface = _get(cfg, 'interfaces', 'external')
int_iface = _get(cfg, 'interfaces', 'internal')
int_addr = _get(cfg, 'local', 'int_addr')
gateway = _get(cfg, 'local', 'int_gateway_ip')
if mode == 'auto':
ext_iface, int_iface = detect_interfaces()
ext_addrs = interface_addresses(ext_iface, 'interfaces.external')
local_addrs = ', '.join(ext_addrs)
if ext_iface and not ext_addrs:
warn(f'{ext_iface} has no IPv4 address yet')
if mode == 'manual':
return ext_iface, int_iface, local_addrs, int_addr, gateway
int_addrs = interface_addresses(int_iface, 'interfaces.internal')
int_addr = int_addrs[0] if int_addrs else ''
if int_iface and not int_addr:
warn(f'{int_iface} has no IPv4 address yet')
gateway = detect_gateway(int_iface, int_addr) if int_addr else ''
return ext_iface, int_iface, local_addrs, int_addr, gateway
def build_context(cfg, local_id_override=None): def build_context(cfg, local_id_override=None):
"""Assemble the template context from the configuration.""" """Assemble the template context from the configuration."""
extras = vpnrouter_platforms.context(platform_module(cfg)) extras = vpnrouter_platforms.context(platform_module(cfg))
ext_iface = _get(cfg, 'interfaces', 'external') ext_iface, int_iface, local_addrs, int_addr, gateway = resolve_interfaces(cfg)
int_iface = _get(cfg, 'interfaces', 'internal')
ext_addrs = interface_addresses(ext_iface, 'interfaces.external')
int_addrs = interface_addresses(int_iface, 'interfaces.internal')
if ext_iface and not ext_addrs:
warn(f'{ext_iface} has no IPv4 address yet')
local_addrs = ', '.join(ext_addrs)
int_addr = int_addrs[0] if int_addrs else ''
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'
@@ -277,7 +409,8 @@ def build_context(cfg, local_id_override=None):
cidr_list = _split_list(local_cidrs) cidr_list = _split_list(local_cidrs)
return { return {
'platform': _get(cfg, 'general', 'platform') or 'generic', 'platform': _get(cfg, 'general', 'platform') or PLATFORM_NONE,
'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': local_addrs,
@@ -287,10 +420,9 @@ def build_context(cfg, local_id_override=None):
'local_id': local_id, 'local_id': local_id,
'local_cidrs': local_cidrs, 'local_cidrs': local_cidrs,
'local_cidr_list': cidr_list, 'local_cidr_list': cidr_list,
'local_subnet': cidr_list[0] if cidr_list else '',
'local_ts': _join(local_cidrs, extras['EXTRA_LOCAL_TS']), 'local_ts': _join(local_cidrs, extras['EXTRA_LOCAL_TS']),
'int_addr': int_addr, 'int_addr': int_addr,
'int_gateway_ip': _get(cfg, 'local', 'int_gateway_ip'), 'int_gateway_ip': gateway,
'remote_addrs': _get(cfg, 'remote', 'addrs'), 'remote_addrs': _get(cfg, 'remote', 'addrs'),
'remote_id': _get(cfg, 'remote', 'id'), 'remote_id': _get(cfg, 'remote', 'id'),
'remote_cidrs': _get(cfg, 'remote', 'cidrs'), 'remote_cidrs': _get(cfg, 'remote', 'cidrs'),
@@ -462,6 +594,12 @@ def configure(cfg=None, local_id_override=None):
""" """
if cfg is None: if cfg is None:
cfg = load_config() cfg = load_config()
# platform = none: configuration is deferred. Files are installed and
# nothing is rendered, provisioned or removed until a platform is chosen.
if deferred(cfg):
return None, set()
try: try:
validate(cfg) validate(cfg)
ctx = build_context(cfg, local_id_override=local_id_override) ctx = build_context(cfg, local_id_override=local_id_override)
@@ -17,13 +17,25 @@
# and leaves the host reachable and unchanged. # and leaves the host reachable and unchanged.
[general] [general]
# Platform module to load: generic, azure or gcp. # Platform: none, generic, azure or gcp.
platform = generic # 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] [interfaces]
# The two inputs everything else is built on. Names only: the addresses on
# these interfaces are read from the system, never configured here.
# External faces the untrusted network; internal faces the protected one. # External faces the untrusted network; internal faces the protected one.
# Required in manual and interfaces mode; worked out for you in auto mode.
external = external =
internal = internal =
@@ -39,7 +51,10 @@ local_id_mode = fqdn
[local] [local]
# Local subnets advertised into the tunnel, comma-separated. # Local subnets advertised into the tunnel, comma-separated.
cidrs = cidrs =
# Next hop on the internal side for the subnets above. # 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 = int_gateway_ip =
[remote] [remote]