222 lines
9.1 KiB
Markdown
222 lines
9.1 KiB
Markdown
# 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
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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/](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:
|
|
|
|
```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.
|
|
|
|
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
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
/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:
|
|
|
|
```sh
|
|
/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:
|
|
|
|
```python
|
|
# /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
|
|
|
|
```sh
|
|
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.
|