Files
linux-cloud-router/README.md
T

9.1 KiB

vpn-router

A Debian package that configures a Linux host or virtual machine as a VPN router:

  • site-to-site IKEv2 IPSec with pre-shared key authentication (strongSwan swanctl)
  • road-warrior (P2S) access using IKEv2 EAP-TLS, with a bundled certificate authority
  • optional WireGuard endpoint
  • 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 name of the external NIC and the name of the internal one; the addresses on them are read from the system rather than configured again. Nothing is auto-detected, because a router that guesses which interface to NAT out of is a router that can guess wrong silently. Platform-specific additions live in separate modules and are selected explicitly.

Installing

The package can be installed by hand or as a step in automated provisioning. Both are supported equally, and both end up with the same two things: a configuration file and a service that applies it.

/etc/vpn-router/vpn-router.conf      the configuration
systemctl restart vpn-router-setup   apply it

By hand

apt-get install vpn-router
$EDITOR /etc/vpn-router/vpn-router.conf
systemctl restart vpn-router-setup

Accepting every default at install time is safe. Installing the package enables IP forwarding and relaxes rp_filter through its own drop-in in /etc/sysctl.d/, which a router needs, but it configures no tunnels, adds no firewall rules and does not enable the firewall. Fill the file in when you are ready.

Automated

Preseed the answers and the router comes up configured, with no follow-up command:

debconf-set-selections <<'EOF'
vpn-router vpn-router/external_interface string eth0
vpn-router vpn-router/internal_interface string eth1
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/int_gateway_ip   string 10.1.1.1
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_cidrs     string 192.168.0.0/24
vpn-router vpn-router/psk              password s3cr3t
EOF
DEBIAN_FRONTEND=noninteractive apt-get install -y vpn-router

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.

See examples/ for a cloud-init template and a shell installer.

Configuration

/etc/vpn-router/vpn-router.conf is INI, mode 0600, and is created once on first install. The package never rewrites it, so it is safe to edit, keep in version control or template. A fully commented 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 _b64 suffix means the value is base64-encoded.

Section Setting Meaning
general platform Platform module: generic, azure or gcp
interfaces external Name of the NIC facing the untrusted network
interfaces internal Name of the NIC facing the protected network
wan local_fqdn This router's FQDN
wan local_id_mode IKE identity source: fqdn, public_ip or internal_ip
local cidrs Local subnets advertised into the tunnel
local int_gateway_ip Next hop on the internal side
remote addrs Remote gateway address(es) or FQDN
remote id Remote IKE identity, without a leading @
remote cidrs Remote subnets reachable through the tunnel
remote psk_b64 Pre-shared key, base64-encoded
remote psk_file Path to a file holding the raw key; wins over psk_b64
p2s enabled Road-warrior access
p2s address_pool Pool assigned to road-warrior clients
p2s ca_name Common name for a locally created CA
wireguard enabled WireGuard endpoint
wireguard address Address and prefix for wg0
wireguard listen_port UDP port, default 51820

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 carries every [Peer] section across untouched.

Encode the pre-shared key with:

printf %s 'the key' | base64 -w0

Every feature is independent. Leaving a group empty simply means that feature is not configured, and its generated files are removed.

The two interface names are the base the rest is built on. The addresses strongSwan binds 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 step with the host. If a named interface does not exist the service stops and says so.

Applying changes

systemctl restart vpn-router-setup

The service also runs at boot. It regenerates the configuration files, reads the current addresses of the two interfaces, applies routes, generates WireGuard keys and injects 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 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 idempotent, so restarting with no changes does nothing.

To regenerate the configuration files without touching the running system:

/usr/lib/vpn-router/configure

Certificates

/etc/vpn-router/pki is the PKI directory, laid out the way simple-ca expects:

ca_cert.pem  ca_key.pem  ca_bundle.pem  <label>_cert.pem  <label>_key.pem  crl.pem

<label> is the first component of the router FQDN, so router.example.com gives router_cert.pem.

If road-warrior access is enabled and this directory is empty, a certificate authority and a server certificate are created there automatically. To use your own instead, place the files in that layout before enabling the feature; nothing is generated or overwritten when material is already present.

Either way, the package copies what each consumer needs into place: the CA into /etc/swanctl/x509ca/, the server certificate and key into /etc/swanctl/x509/ and /etc/swanctl/private/, a CRL into /etc/swanctl/x509crl/, and the CA into the system trust store.

Issue a client certificate with the bundled tool:

/usr/lib/vpn-router/simple-ca make-cert --ca-dir /etc/vpn-router/pki alice
/usr/lib/vpn-router/simple-ca make-pfx  --ca-dir /etc/vpn-router/pki \
    --password s3cr3t /etc/vpn-router/pki/alice_cert.pem

simple-ca comes from a separate project, https://gitea.koszewscy.waw.pl/slawek/simple-ca, and also supports make-crl and revoke-cert.

Platform modules

Platform-specific values do not belong in the core, so they are Python modules in /usr/lib/vpn-router/vpnrouter_platforms/, imported at run time and selected by general.platform. The platform is never probed: the caller already knows where it is deploying.

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 no provider resolver has to be advertised or routed through the tunnel on any platform. The modules exist as the documented place for platform specifics if that changes.

A module may define any of the following, and takes the default for anything it omits:

Name Type Effect
EXTRA_P2S_DNS str added to the road-warrior pool dns list
EXTRA_LOCAL_TS str added to local_ts on both connections
apply(ctx) function called once the system is configured, to apply platform-specific state; must be idempotent

ctx is the template context, so a module reads what it needs from it - ctx['wan_iface'], ctx['int_iface'], ctx['local_cidrs'] and the rest.

Adding a platform is adding one file. Modules are discovered by listing the package, so nothing in the core needs editing:

# /usr/lib/vpn-router/vpnrouter_platforms/example.py
"""Example provider."""

EXTRA_P2S_DNS = '192.0.2.53'
EXTRA_LOCAL_TS = '192.0.2.53/32'

Set platform = example and restart the service. The debconf question offers only the modules shipped with the package; the configuration file accepts any module installed.

Removal

apt-get remove terminates the tunnels and takes wg-quick@wg0 down, but leaves the configuration in place. strongswan is left enabled, since it is a shared service that may be in use by something else.

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/vpn-router/pki, the WireGuard key pair and everything under /etc/swanctl are left alone.

Building

cd debian-package
./build.sh

The build runs in a container and writes the package to debian-package/out/. ./publish.sh uploads it to the configured repository.