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.
265 lines
12 KiB
Markdown
265 lines
12 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 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.
|
|
|
|
## 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 <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.
|
|
|
|
`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
|
|
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, 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
|
|
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.
|