# 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 the external and internal NIC names, and a `mode` setting says how much of the rest you are supplying and how much is read from the system. Detection happens only in the mode that asks for it, so 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. 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 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 ```sh apt-get install vpn-router # answer "none" to the platform question $EDITOR /etc/vpn-router/vpn-router.conf systemctl restart vpn-router-setup ``` Answering `none` to the platform question defers everything: no further questions are asked and nothing on the machine is configured. The only change an install makes is the package's own `/etc/sysctl.d/` drop-in, which enables IP forwarding and relaxes `rp_filter` as a router needs. Set `platform` in the file when you are ready. ### Automated 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: ```shell 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/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_cidrs string 10.0.0.0/24 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 ``` 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. 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 [examples/azure/](examples/azure/) / [examples/gcp/](examples/gcp/) for full Terraform examples that deploy a router on Azure or GCP. ## Configuration `/etc/vpn-router/vpn-router.conf` is INI, 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 changed and asks before touching anything you altered by hand. 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` | `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` | `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_addr` | This host's address on the internal network | | `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: ```sh 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. ### What `mode` decides `mode` states how much of the network configuration you are supplying, and therefore how 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 ```sh systemctl restart vpn-router-setup ``` 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 addresses as `mode` directs, 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 and PKI without starting, stopping or reloading anything: ```sh /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 `/etc/vpn-router/pki` is the PKI directory, laid out the way `simple-ca` expects: ``` ca_cert.pem ca_key.pem ca_bundle.pem