341 lines
16 KiB
Markdown
341 lines
16 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
|
|
- firewall rules for the ports Docker publishes, when Docker is installed
|
|
|
|
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.json the configuration
|
|
systemctl restart vpn-router-setup apply it
|
|
```
|
|
|
|
If the host runs Docker, the recommended order is Docker first and the package second. The
|
|
package secures the ports Docker publishes when it finds the Docker service; the steps for a
|
|
host that already runs Docker, and for installing Docker later, are under [Docker](#docker).
|
|
|
|
### By hand
|
|
|
|
```sh
|
|
apt-get install vpn-router # answer "none" to the platform question
|
|
$EDITOR /etc/vpn-router/vpn-router.json
|
|
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.json` 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.json` is JSON, 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 complete example is
|
|
installed at `/usr/share/doc/vpn-router/vpn-router.json.example`.
|
|
|
|
The file is an object of sections, each an object of settings. A section or setting that
|
|
is left out takes its default, and an empty string or empty list means unset. Every value
|
|
must have the type shown below; an unknown section or setting is an error.
|
|
|
|
| Section | Setting | Type | Default | Meaning |
|
|
|---|---|---|---|---|
|
|
| `general` | `platform` | string | `none` | `none`, `generic`, `azure` or `gcp`. `none` defers configuration entirely |
|
|
| `general` | `mode` | string | `manual` | How much you supply: `manual`, `interfaces` or `auto` |
|
|
| `interfaces` | `external` | string | | Name of the NIC facing the untrusted network |
|
|
| `interfaces` | `internal` | string | | Name of the NIC facing the protected network |
|
|
| `wan` | `local_fqdn` | string | | This router's FQDN |
|
|
| `wan` | `local_id_mode` | string | `fqdn` | IKE identity source: `fqdn`, `public_ip` or `internal_ip` |
|
|
| `local` | `cidrs` | list | | Local subnets advertised into the tunnel |
|
|
| `local` | `int_addr` | string | | This host's address on the internal network |
|
|
| `local` | `int_gateway_ip` | string | | Next hop on the internal side |
|
|
| `remote` | `addrs` | list | | Remote gateway addresses or FQDNs |
|
|
| `remote` | `id` | string | | Remote IKE identity, without a leading `@` |
|
|
| `remote` | `cidrs` | list | | Remote subnets reachable through the tunnel |
|
|
| `remote` | `psk` | string | | Pre-shared key |
|
|
| `remote` | `psk_file` | string | | Path to a file holding the key; wins over `psk` |
|
|
| `p2s` | `enabled` | boolean | `false` | Road-warrior access |
|
|
| `p2s` | `address_pool` | string | | CIDR assigned to road-warrior clients |
|
|
| `p2s` | `ca_name` | string | `VPN Router CA` | Common name for a locally created CA |
|
|
| `wireguard` | `enabled` | boolean | `false` | WireGuard endpoint |
|
|
| `wireguard` | `address` | string | | Address and prefix for `wg0`, for example `192.168.200.1/24` |
|
|
| `wireguard` | `listen_port` | integer | `51820` | UDP port |
|
|
| `docker` | `mode` | string | `auto` | `auto` secures Docker when its service is installed, `on` requires it, `off` removes the rules |
|
|
| `docker` | `exposed_ports` | list | | TCP ports and port ranges (`8000:8100`) reachable from the external interface; at most 15, a range counting as two |
|
|
|
|
A minimal site-to-site configuration in `auto` mode:
|
|
|
|
```json
|
|
{
|
|
"general": { "platform": "generic", "mode": "auto" },
|
|
"wan": { "local_fqdn": "router.example.com" },
|
|
"local": { "cidrs": ["10.0.0.0/24"] },
|
|
"remote": {
|
|
"addrs": ["peer.example.net"],
|
|
"id": "peer.example.net",
|
|
"cidrs": ["192.168.0.0/24"],
|
|
"psk": "s3cr3t"
|
|
}
|
|
}
|
|
```
|
|
|
|
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.
|
|
|
|
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: the external one from the routing table, the internal one as the only other NIC with an address |
|
|
|
|
`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.
|
|
|
|
Detection ignores virtual interfaces, those the kernel lists under `/sys/devices/virtual`:
|
|
loopback, bridges such as `docker0`, veth pairs and tunnels. Only the NICs of the machine
|
|
count, so Docker does not change the result. If the host has more than two NICs with an
|
|
address, `auto` stops and says so; use mode `interfaces` and name them.
|
|
|
|
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.
|
|
|
|
## Docker
|
|
|
|
Docker publishes container ports in its own firewall rules, which bypass UFW and can expose
|
|
ports on the external interface. Docker evaluates the `DOCKER-USER` chain before its own
|
|
rules, so the package filters there. With `docker.mode` set to `auto`, the package secures
|
|
Docker when `docker.service` is installed, and does nothing otherwise.
|
|
|
|
### On a host that already runs Docker
|
|
|
|
Installing Docker first is the recommended order. After installing the package:
|
|
|
|
1. Set `general.platform` in `/etc/vpn-router/vpn-router.json`. While it is `none`, nothing is
|
|
configured, the Docker rules included.
|
|
2. List the TCP ports the running containers must expose to the internet, for example
|
|
`"docker": { "exposed_ports": ["443"] }`. The default is none: once the rules are applied,
|
|
every published port that is not listed is unreachable from the external interface, and
|
|
remains reachable through the IPsec tunnel.
|
|
3. Apply the configuration with `systemctl restart vpn-router-setup`. Docker is running, so
|
|
this also restarts `vpn-router-docker`, which installs the rules.
|
|
4. Check the result:
|
|
|
|
```sh
|
|
iptables -S DOCKER-USER # "-j VPN-ROUTER-DOCKER" is the first rule
|
|
iptables -S VPN-ROUTER-DOCKER # the rules of the external interface
|
|
```
|
|
|
|
### When Docker is installed after the package
|
|
|
|
The Docker rules are not applied when Docker starts for the first time, so run
|
|
`systemctl restart vpn-router-setup` after installing Docker, then check as above. A reboot or a
|
|
restart of Docker applies them as well.
|
|
|
|
The rules sit in their own chain, `VPN-ROUTER-DOCKER`, entered by one jump at the top of
|
|
`DOCKER-USER`; other rules in `DOCKER-USER` are left alone. For traffic that arrives on the
|
|
external interface, in this order:
|
|
|
|
1. IPsec protected traffic is accepted.
|
|
2. TCP to the ports in `docker.exposed_ports` is accepted; none by default.
|
|
3. Packets of established and related connections are accepted.
|
|
4. Everything else is dropped.
|
|
|
|
Traffic from every other interface, including the internal one and the Docker bridges, is
|
|
not affected. The rules cover IPv4 only.
|
|
|
|
The `vpn-router-docker` service applies the rules after `docker.service` starts and removes
|
|
them when Docker stops, so restarting Docker restores them. `systemctl restart
|
|
vpn-router-setup` applies a changed configuration while Docker runs. With `docker.mode` set to
|
|
`on`, the service fails when Docker is not installed or its daemon is not running; with
|
|
`off` it removes the rules.
|
|
|
|
## 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 `general.platform` to `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` stops `vpn-router-docker`, which removes the Docker rules. It 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.json` 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.
|