Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64da0cf97f | ||
|
|
2e0cee10ac | ||
|
|
66fcd69d40 | ||
|
|
19567aa273 | ||
|
|
656e1733d5 | ||
|
|
b18947ed9b |
No files matched your search
@@ -0,0 +1,27 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*.*.*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Build package
|
||||||
|
working-directory: debian-package
|
||||||
|
run: ./build.sh
|
||||||
|
|
||||||
|
- name: Publish package
|
||||||
|
working-directory: debian-package
|
||||||
|
env:
|
||||||
|
SERVER_URL: ${{ secrets.CI_URL }}
|
||||||
|
OWNER: ${{ secrets.CI_OWNER }}
|
||||||
|
PACKAGES_TOKEN: ${{ secrets.CI_TOKEN }}
|
||||||
|
run: ./publish.sh
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Architecture Decision Records
|
||||||
|
|
||||||
|
## ADR-1: Docker's published ports are filtered in DOCKER-USER by the package
|
||||||
|
|
||||||
|
Status: Accepted
|
||||||
|
|
||||||
|
Context: Docker adds its own iptables rules for published ports. They are evaluated before
|
||||||
|
UFW's, so a published port is reachable from the external interface whatever UFW says. Docker
|
||||||
|
reserves the `DOCKER-USER` chain, evaluated first in `FORWARD`, for rules of the operator. The
|
||||||
|
router is an internet-facing host, and Docker is installed on it for services run alongside the
|
||||||
|
router.
|
||||||
|
|
||||||
|
Decision: The package secures Docker itself. A `docker` configuration section has `mode`
|
||||||
|
(`auto`, `on`, `off`) and `exposed_ports`. In `auto` mode the rules are applied when
|
||||||
|
`docker.service` is installed, which is what the package looks up; `on` requires Docker and
|
||||||
|
`off` removes the rules. The rules are in a chain of their own, `VPN-ROUTER-DOCKER`, entered by
|
||||||
|
one jump at the top of `DOCKER-USER`. For traffic from the external interface they accept IPsec
|
||||||
|
protected traffic, TCP to `exposed_ports` and established connections, and drop the rest. The
|
||||||
|
`vpn-router-docker` unit applies them after `docker.service` starts and removes them when it
|
||||||
|
stops; `vpn-router-setup` restarts it while Docker runs.
|
||||||
|
|
||||||
|
Consequences: Installing Docker first is recommended; `auto` mode then finds it as soon as the
|
||||||
|
package is configured. When Docker is installed after the package, the rules are applied by
|
||||||
|
restarting `vpn-router-setup`, a reboot or a restart of Docker, not by Docker's first start,
|
||||||
|
because `vpn-router-docker` is already active. A published
|
||||||
|
port is reachable from the external interface only when it is in `exposed_ports`. Other rules
|
||||||
|
in `DOCKER-USER` are not touched, because the package owns only its own chain. Restarting Docker
|
||||||
|
restores the rules. The rules cover IPv4 only.
|
||||||
@@ -6,6 +6,7 @@ A Debian package that configures a Linux host or virtual machine as a VPN router
|
|||||||
- road-warrior (P2S) access using IKEv2 EAP-TLS, with a bundled certificate authority
|
- road-warrior (P2S) access using IKEv2 EAP-TLS, with a bundled certificate authority
|
||||||
- optional WireGuard endpoint
|
- optional WireGuard endpoint
|
||||||
- routing and NAT for a protected subnet, TCP MSS clamping, and UFW firewall rules
|
- 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
|
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
|
internal NIC names, and a `mode` setting says how much of the rest you are supplying and
|
||||||
@@ -24,15 +25,19 @@ supported equally, and both end up with the same two things: a configuration fil
|
|||||||
service that applies it.
|
service that applies it.
|
||||||
|
|
||||||
```
|
```
|
||||||
/etc/vpn-router/vpn-router.conf the configuration
|
/etc/vpn-router/vpn-router.json the configuration
|
||||||
systemctl restart vpn-router-setup apply it
|
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
|
### By hand
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
apt-get install vpn-router # answer "none" to the platform question
|
apt-get install vpn-router # answer "none" to the platform question
|
||||||
$EDITOR /etc/vpn-router/vpn-router.conf
|
$EDITOR /etc/vpn-router/vpn-router.json
|
||||||
systemctl restart vpn-router-setup
|
systemctl restart vpn-router-setup
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -65,10 +70,10 @@ EOF
|
|||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y vpn-router
|
DEBIAN_FRONTEND=noninteractive apt-get install -y vpn-router
|
||||||
```
|
```
|
||||||
|
|
||||||
With `mode = interfaces` the `int_addr` and `int_gateway_ip` lines are dropped and read
|
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.
|
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.
|
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`.
|
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
|
See [examples/](examples/) for a cloud-init template and a shell installer, and
|
||||||
@@ -77,47 +82,60 @@ that deploy a router on Azure or GCP.
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
`/etc/vpn-router/vpn-router.conf` is INI, mode 0600, and is created on first install. It
|
`/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
|
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
|
changed and asks before touching anything you altered by hand. A complete example is
|
||||||
reference is installed at `/usr/share/doc/vpn-router/vpn-router.conf.example`.
|
installed at `/usr/share/doc/vpn-router/vpn-router.json.example`.
|
||||||
|
|
||||||
Lists are comma-separated, booleans are `true`/`false`, an empty value means unset, and a
|
The file is an object of sections, each an object of settings. A section or setting that
|
||||||
`_b64` suffix means the value is base64-encoded.
|
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 | Meaning |
|
| Section | Setting | Type | Default | Meaning |
|
||||||
|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `general` | `platform` | `none`, `generic`, `azure` or `gcp`. `none` defers configuration entirely |
|
| `general` | `platform` | string | `none` | `none`, `generic`, `azure` or `gcp`. `none` defers configuration entirely |
|
||||||
| `general` | `mode` | How much you supply: `manual`, `interfaces` or `auto` |
|
| `general` | `mode` | string | `manual` | How much you supply: `manual`, `interfaces` or `auto` |
|
||||||
| `interfaces` | `external` | Name of the NIC facing the untrusted network |
|
| `interfaces` | `external` | string | | Name of the NIC facing the untrusted network |
|
||||||
| `interfaces` | `internal` | Name of the NIC facing the protected network |
|
| `interfaces` | `internal` | string | | Name of the NIC facing the protected network |
|
||||||
| `wan` | `local_fqdn` | This router's FQDN |
|
| `wan` | `local_fqdn` | string | | This router's FQDN |
|
||||||
| `wan` | `local_id_mode` | IKE identity source: `fqdn`, `public_ip` or `internal_ip` |
|
| `wan` | `local_id_mode` | string | `fqdn` | IKE identity source: `fqdn`, `public_ip` or `internal_ip` |
|
||||||
| `local` | `cidrs` | Local subnets advertised into the tunnel |
|
| `local` | `cidrs` | list | | Local subnets advertised into the tunnel |
|
||||||
| `local` | `int_addr` | This host's address on the internal network |
|
| `local` | `int_addr` | string | | This host's address on the internal network |
|
||||||
| `local` | `int_gateway_ip` | Next hop on the internal side |
|
| `local` | `int_gateway_ip` | string | | Next hop on the internal side |
|
||||||
| `remote` | `addrs` | Remote gateway address(es) or FQDN |
|
| `remote` | `addrs` | list | | Remote gateway addresses or FQDNs |
|
||||||
| `remote` | `id` | Remote IKE identity, without a leading `@` |
|
| `remote` | `id` | string | | Remote IKE identity, without a leading `@` |
|
||||||
| `remote` | `cidrs` | Remote subnets reachable through the tunnel |
|
| `remote` | `cidrs` | list | | Remote subnets reachable through the tunnel |
|
||||||
| `remote` | `psk_b64` | Pre-shared key, base64-encoded |
|
| `remote` | `psk` | string | | Pre-shared key |
|
||||||
| `remote` | `psk_file` | Path to a file holding the raw key; wins over `psk_b64` |
|
| `remote` | `psk_file` | string | | Path to a file holding the key; wins over `psk` |
|
||||||
| `p2s` | `enabled` | Road-warrior access |
|
| `p2s` | `enabled` | boolean | `false` | Road-warrior access |
|
||||||
| `p2s` | `address_pool` | Pool assigned to road-warrior clients |
|
| `p2s` | `address_pool` | string | | CIDR assigned to road-warrior clients |
|
||||||
| `p2s` | `ca_name` | Common name for a locally created CA |
|
| `p2s` | `ca_name` | string | `VPN Router CA` | Common name for a locally created CA |
|
||||||
| `wireguard` | `enabled` | WireGuard endpoint |
|
| `wireguard` | `enabled` | boolean | `false` | WireGuard endpoint |
|
||||||
| `wireguard` | `address` | Address and prefix for `wg0` |
|
| `wireguard` | `address` | string | | Address and prefix for `wg0`, for example `192.168.200.1/24` |
|
||||||
| `wireguard` | `listen_port` | UDP port, default 51820 |
|
| `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
|
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
|
`[Interface]` section of that file and rewrites it when the settings above change, but it
|
||||||
carries every `[Peer]` section across untouched.
|
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
|
Every feature is independent. Leaving a group empty simply means that feature is not
|
||||||
configured, and its generated files are removed.
|
configured, and its generated files are removed.
|
||||||
|
|
||||||
@@ -130,25 +148,80 @@ much the package reads from the system:
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `manual` | both interface names, `int_addr`, `int_gateway_ip` | nothing |
|
| `manual` | both interface names, `int_addr`, `int_gateway_ip` | nothing |
|
||||||
| `interfaces` | both interface names | `int_addr` and `int_gateway_ip`, from those interfaces |
|
| `interfaces` | both interface names | `int_addr` and `int_gateway_ip`, from those interfaces |
|
||||||
| `auto` | nothing | the interface names too, from the routing table |
|
| `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
|
`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
|
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.
|
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
|
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
|
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
|
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
|
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.
|
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
|
## Applying changes
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
systemctl restart vpn-router-setup
|
systemctl restart vpn-router-setup
|
||||||
```
|
```
|
||||||
|
|
||||||
The service also runs at boot. With `platform = none` it does nothing and says so.
|
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
|
Otherwise it regenerates the configuration files, resolves the interfaces and their
|
||||||
addresses as `mode` directs, applies routes, generates WireGuard keys and injects
|
addresses as `mode` directs, applies routes, generates WireGuard keys and injects
|
||||||
firewall rules. It then starts `strongswan` and `wg-quick@wg0` if the corresponding
|
firewall rules. It then starts `strongswan` and `wg-quick@wg0` if the corresponding
|
||||||
@@ -240,17 +313,18 @@ EXTRA_P2S_DNS = '192.0.2.53'
|
|||||||
EXTRA_LOCAL_TS = '192.0.2.53/32'
|
EXTRA_LOCAL_TS = '192.0.2.53/32'
|
||||||
```
|
```
|
||||||
|
|
||||||
Set `platform = example` and restart the service. The debconf question offers only the
|
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.
|
modules shipped with the package; the configuration file accepts any module installed.
|
||||||
|
|
||||||
## Removal
|
## Removal
|
||||||
|
|
||||||
`apt-get remove` terminates the tunnels and takes `wg-quick@wg0` down, but leaves the
|
`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
|
configuration in place. `strongswan` is left enabled, since it is a shared service that
|
||||||
may be in use by something else.
|
may be in use by something else.
|
||||||
|
|
||||||
`apt-get purge` additionally removes the generated files and the firewall rules, restoring
|
`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`
|
`/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:
|
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
|
`/etc/vpn-router/pki`, the WireGuard key pair and everything under `/etc/swanctl` are left
|
||||||
alone.
|
alone.
|
||||||
|
|||||||
Whitespace-only changes.
+10
-2
@@ -1,9 +1,17 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
# Apple container CLI on macOS, Docker CE on Linux.
|
||||||
|
case "$(uname -s)" in
|
||||||
|
Darwin) CONTAINER_RUNTIME=container ;;
|
||||||
|
Linux) CONTAINER_RUNTIME=docker ;;
|
||||||
|
*) printf 'ERROR: unsupported platform %s\n' "$(uname -s)" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
mkdir -p out
|
mkdir -p out
|
||||||
|
|
||||||
container build --build-arg HOST_UID=$(id -u) --build-arg HOST_GID=$(id -g) -t vpn-router-builder "$@" .
|
$CONTAINER_RUNTIME build --build-arg HOST_UID=$(id -u) --build-arg HOST_GID=$(id -g) -t vpn-router-builder "$@" .
|
||||||
container run --rm -v "$(pwd)/out:/mnt" vpn-router-builder sh -c 'cp -p /out/* /mnt/'
|
# Artifacts are streamed out as a tar archive, so no bind mount is needed.
|
||||||
|
$CONTAINER_RUNTIME run --rm vpn-router-builder tar -cf - -C /out . | tar -xf - -C out
|
||||||
|
|
||||||
echo "Build artifacts written to out/"
|
echo "Build artifacts written to out/"
|
||||||
@@ -1,4 +1,53 @@
|
|||||||
vpn-router (1.0.0-1) unstable; urgency=medium
|
vpn-router (0.3.1-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Ignore virtual interfaces when mode auto looks for the internal
|
||||||
|
interface. The Docker bridge docker0, which has an address, was counted
|
||||||
|
as a second internal interface, so vpn-router-setup and
|
||||||
|
vpn-router-docker failed on a host with Docker installed.
|
||||||
|
|
||||||
|
-- Sławomir Koszewski <slawek@koszewscy.waw.pl> Fri, 23 Oct 2026 13:44:00 +0200
|
||||||
|
|
||||||
|
vpn-router (0.3.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Secure the ports Docker publishes. Rules in a chain of their own,
|
||||||
|
VPN-ROUTER-DOCKER, entered by a jump at the top of DOCKER-USER, accept
|
||||||
|
IPsec protected traffic, TCP to docker.exposed_ports and established
|
||||||
|
connections from the external interface and drop the rest of it. Other
|
||||||
|
rules in DOCKER-USER are left alone. The rules cover IPv4 only.
|
||||||
|
* Add the docker configuration section. docker.mode is auto (the default,
|
||||||
|
secures Docker when docker.service is installed), on (requires Docker) or
|
||||||
|
off (removes the rules); docker.exposed_ports lists the TCP ports and port
|
||||||
|
ranges reachable from the external interface, none by default.
|
||||||
|
* Add the vpn-router-docker service, which applies the Docker rules after
|
||||||
|
docker.service starts and removes them when it stops. vpn-router-setup
|
||||||
|
restarts it while Docker runs, so a changed configuration takes effect.
|
||||||
|
* Installing Docker before this package is recommended. When Docker is
|
||||||
|
installed later, restart vpn-router-setup to apply the rules.
|
||||||
|
|
||||||
|
-- Sławomir Koszewski <slawek@koszewscy.waw.pl> Fri, 23 Oct 2026 12:44:45 +0200
|
||||||
|
|
||||||
|
vpn-router (0.2.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Change the configuration file to JSON, /etc/vpn-router/vpn-router.json.
|
||||||
|
Lists are arrays, booleans and the WireGuard listen port are native JSON
|
||||||
|
types, and unknown sections, unknown settings and wrong value types are
|
||||||
|
rejected.
|
||||||
|
* Replace remote.psk_b64 with remote.psk, holding the pre-shared key as a
|
||||||
|
plain string. remote.psk_file still takes precedence.
|
||||||
|
* Ship /usr/share/doc/vpn-router/vpn-router.json.example in place of
|
||||||
|
vpn-router.conf.example.
|
||||||
|
* Add platform "none" as the default, so installing the package changes
|
||||||
|
nothing on the machine until a platform is chosen.
|
||||||
|
* Add the mode setting (manual, interfaces, auto) controlling how much of
|
||||||
|
the network configuration is supplied and how much is read from the
|
||||||
|
system.
|
||||||
|
* Manage the configuration file with ucf, so dpkg-reconfigure reapplies
|
||||||
|
debconf answers without overwriting local edits.
|
||||||
|
* Apply NAT and forward rules to every local subnet, not only the first.
|
||||||
|
|
||||||
|
-- Sławomir Koszewski <slawek@koszewscy.waw.pl> Fri, 23 Oct 2026 08:00:06 +0200
|
||||||
|
|
||||||
|
vpn-router (0.1.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
* Initial release.
|
* Initial release.
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ Description: Linux VPN router with IPSec, road-warrior and WireGuard support
|
|||||||
optional WireGuard, and the routing, NAT, MSS clamping and firewall rules a
|
optional WireGuard, and the routing, NAT, MSS clamping and firewall rules a
|
||||||
router needs. Nothing in the package is specific to a cloud provider.
|
router needs. Nothing in the package is specific to a cloud provider.
|
||||||
.
|
.
|
||||||
Settings live in /etc/vpn-router/vpn-router.conf, which is created on first
|
Settings live in /etc/vpn-router/vpn-router.json, which is created on first
|
||||||
install and owned by the administrator afterwards. Editing it and restarting
|
install and owned by the administrator afterwards. Editing it and restarting
|
||||||
vpn-router-setup applies the change; the same service reconciles the system
|
vpn-router-setup applies the change; the same service reconciles the system
|
||||||
at every boot. It is built on two inputs, the external and internal interface
|
at every boot. It is built on two inputs, the external and internal interface
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ src/etc/sysctl.d/99-vpn-router.conf etc/sysctl.d/
|
|||||||
src/usr/lib/vpn-router/vpnrouter.py usr/lib/vpn-router/
|
src/usr/lib/vpn-router/vpnrouter.py usr/lib/vpn-router/
|
||||||
src/usr/lib/vpn-router/simple-ca usr/lib/vpn-router/
|
src/usr/lib/vpn-router/simple-ca usr/lib/vpn-router/
|
||||||
src/usr/lib/vpn-router/configure usr/lib/vpn-router/
|
src/usr/lib/vpn-router/configure usr/lib/vpn-router/
|
||||||
|
src/usr/lib/vpn-router/docker-rules usr/lib/vpn-router/
|
||||||
src/usr/lib/vpn-router/setup usr/lib/vpn-router/
|
src/usr/lib/vpn-router/setup usr/lib/vpn-router/
|
||||||
src/usr/lib/vpn-router/generate-config usr/lib/vpn-router/
|
src/usr/lib/vpn-router/generate-config usr/lib/vpn-router/
|
||||||
src/usr/lib/vpn-router/vpnrouter_platforms/*.py usr/lib/vpn-router/vpnrouter_platforms/
|
src/usr/lib/vpn-router/vpnrouter_platforms/*.py usr/lib/vpn-router/vpnrouter_platforms/
|
||||||
src/usr/share/vpn-router/templates/* usr/share/vpn-router/templates/
|
src/usr/share/vpn-router/templates/* usr/share/vpn-router/templates/
|
||||||
src/usr/share/doc/vpn-router/vpn-router.conf.example usr/share/doc/vpn-router/
|
src/usr/share/doc/vpn-router/vpn-router.json.example usr/share/doc/vpn-router/
|
||||||
@@ -42,9 +42,9 @@ case "$1" in
|
|||||||
# This is what makes dpkg-reconfigure apply without destroying edits.
|
# This is what makes dpkg-reconfigure apply without destroying edits.
|
||||||
CANDIDATE="$(mktemp)"
|
CANDIDATE="$(mktemp)"
|
||||||
/usr/lib/vpn-router/generate-config "$CANDIDATE"
|
/usr/lib/vpn-router/generate-config "$CANDIDATE"
|
||||||
ucf --three-way --debconf-ok "$CANDIDATE" /etc/vpn-router/vpn-router.conf
|
ucf --three-way --debconf-ok "$CANDIDATE" /etc/vpn-router/vpn-router.json
|
||||||
ucfr vpn-router /etc/vpn-router/vpn-router.conf
|
ucfr vpn-router /etc/vpn-router/vpn-router.json
|
||||||
chmod 0600 /etc/vpn-router/vpn-router.conf
|
chmod 0600 /etc/vpn-router/vpn-router.json
|
||||||
rm -f "$CANDIDATE"
|
rm -f "$CANDIDATE"
|
||||||
|
|
||||||
# The key now lives in the configuration file; do not keep a copy. An
|
# The key now lives in the configuration file; do not keep a copy. An
|
||||||
|
|||||||
@@ -37,17 +37,17 @@ case "$1" in
|
|||||||
# Let ucf forget the file before it is removed, or a reinstall finds a
|
# Let ucf forget the file before it is removed, or a reinstall finds a
|
||||||
# stale hash and declines to lay the file down again.
|
# stale hash and declines to lay the file down again.
|
||||||
if command -v ucf >/dev/null 2>&1; then
|
if command -v ucf >/dev/null 2>&1; then
|
||||||
ucf --purge /etc/vpn-router/vpn-router.conf
|
ucf --purge /etc/vpn-router/vpn-router.json
|
||||||
fi
|
fi
|
||||||
if command -v ucfr >/dev/null 2>&1; then
|
if command -v ucfr >/dev/null 2>&1; then
|
||||||
ucfr --purge vpn-router /etc/vpn-router/vpn-router.conf
|
ucfr --purge vpn-router /etc/vpn-router/vpn-router.json
|
||||||
fi
|
fi
|
||||||
|
|
||||||
rm -f /etc/swanctl/conf.d/remote-site.conf \
|
rm -f /etc/swanctl/conf.d/remote-site.conf \
|
||||||
/etc/swanctl/conf.d/road-warrior.conf \
|
/etc/swanctl/conf.d/road-warrior.conf \
|
||||||
/etc/systemd/resolved.conf.d/p2s-forwarder.conf \
|
/etc/systemd/resolved.conf.d/p2s-forwarder.conf \
|
||||||
/etc/wireguard/wg0.conf \
|
/etc/wireguard/wg0.conf \
|
||||||
/etc/vpn-router/vpn-router.conf
|
/etc/vpn-router/vpn-router.json
|
||||||
|
|
||||||
# Key material is not package state: /etc/vpn-router/pki, the WireGuard
|
# Key material is not package state: /etc/vpn-router/pki, the WireGuard
|
||||||
# key pair and everything under /etc/swanctl are left in place.
|
# key pair and everything under /etc/swanctl are left in place.
|
||||||
|
|||||||
@@ -4,3 +4,4 @@
|
|||||||
|
|
||||||
override_dh_installsystemd:
|
override_dh_installsystemd:
|
||||||
dh_installsystemd --name=vpn-router-setup
|
dh_installsystemd --name=vpn-router-setup
|
||||||
|
dh_installsystemd --name=vpn-router-docker
|
||||||
@@ -9,7 +9,7 @@ Description: Platform:
|
|||||||
none defers configuration entirely: the package installs its files and
|
none defers configuration entirely: the package installs its files and
|
||||||
changes nothing on the machine. Choose it when the router will be configured
|
changes nothing on the machine. Choose it when the router will be configured
|
||||||
later by hand or by a configuration-management tool, then set the platform in
|
later by hand or by a configuration-management tool, then set the platform in
|
||||||
/etc/vpn-router/vpn-router.conf when you are ready.
|
/etc/vpn-router/vpn-router.json when you are ready.
|
||||||
.
|
.
|
||||||
generic configures the router with no platform-specific additions.
|
generic configures the router with no platform-specific additions.
|
||||||
|
|
||||||
@@ -113,8 +113,8 @@ Template: vpn-router/psk
|
|||||||
Type: password
|
Type: password
|
||||||
Description: Pre-shared key (PSK):
|
Description: Pre-shared key (PSK):
|
||||||
Pre-shared key for the site-to-site IKEv2 tunnel. Must match the value
|
Pre-shared key for the site-to-site IKEv2 tunnel. Must match the value
|
||||||
configured on the remote peer. Stored base64-encoded in the configuration
|
configured on the remote peer. Stored in the configuration file and cleared
|
||||||
file and cleared from the debconf database after installation.
|
from the debconf database after installation.
|
||||||
|
|
||||||
Template: vpn-router/p2s_enabled
|
Template: vpn-router/p2s_enabled
|
||||||
Type: boolean
|
Type: boolean
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=VPN Router rules for Docker published ports
|
||||||
|
Documentation=file:/usr/share/doc/vpn-router/README.md
|
||||||
|
After=docker.service vpn-router-setup.service
|
||||||
|
PartOf=docker.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
RemainAfterExit=yes
|
||||||
|
ExecStart=/usr/lib/vpn-router/docker-rules apply
|
||||||
|
ExecStop=/usr/lib/vpn-router/docker-rules remove
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=docker.service
|
||||||
@@ -1,9 +1,21 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
# Apple container CLI on macOS, Docker CE on Linux.
|
||||||
|
case "$(uname -s)" in
|
||||||
|
Darwin) CONTAINER_RUNTIME=container ;;
|
||||||
|
Linux) CONTAINER_RUNTIME=docker ;;
|
||||||
|
*) printf 'ERROR: unsupported platform %s\n' "$(uname -s)" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
OWNER="${OWNER:-slawek}"
|
||||||
|
SERVER_URL="${SERVER_URL:-https://gitea.koszewscy.waw.pl}"
|
||||||
|
PACKAGES_TOKEN="${PACKAGES_TOKEN:-$(cat ~/.gitea-packages-token)}"
|
||||||
|
|
||||||
|
# Reads the changelog copied into the builder image, so no bind mount is needed.
|
||||||
changelog_field() {
|
changelog_field() {
|
||||||
container run --rm -v "$(pwd):/mnt" vpn-router-builder \
|
$CONTAINER_RUNTIME run --rm vpn-router-builder \
|
||||||
dpkg-parsechangelog --show-field "$1" -l /mnt/debian/changelog
|
dpkg-parsechangelog --show-field "$1" -l /build/src/debian/changelog
|
||||||
}
|
}
|
||||||
|
|
||||||
VERSION="$(changelog_field Version)"
|
VERSION="$(changelog_field Version)"
|
||||||
@@ -15,6 +27,14 @@ if [ ! -f "$DEB" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
curl -v --user "slawek:$(cat ~/.gitea-packages-token)" \
|
STATUS="$(curl -s -o /dev/null -w '%{http_code}' --user "$OWNER:$PACKAGES_TOKEN" \
|
||||||
|
"$SERVER_URL/api/v1/packages/$OWNER/debian/$PACKAGE/$VERSION")"
|
||||||
|
case "$STATUS" in
|
||||||
|
200) printf '%s %s is already published, skipping.\n' "$PACKAGE" "$VERSION"; exit 0 ;;
|
||||||
|
404) ;;
|
||||||
|
*) printf 'ERROR: version check returned HTTP %s.\n' "$STATUS" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
curl --fail-with-body --user "$OWNER:$PACKAGES_TOKEN" \
|
||||||
--upload-file "$DEB" \
|
--upload-file "$DEB" \
|
||||||
"https://gitea.koszewscy.waw.pl/api/packages/slawek/debian/pool/noble/main/upload"
|
"$SERVER_URL/api/packages/$OWNER/debian/pool/noble/main/upload"
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
"""Render configuration files and provision PKI from vpn-router.conf.
|
"""Render configuration files and provision PKI from vpn-router.json.
|
||||||
|
|
||||||
Run directly to regenerate them without starting, stopping or reloading
|
Run directly to regenerate them without starting, stopping or reloading
|
||||||
anything. This still writes to /etc/swanctl, /etc/systemd/resolved.conf.d and
|
anything. This still writes to /etc/swanctl, /etc/systemd/resolved.conf.d and
|
||||||
|
|||||||
+114
@@ -0,0 +1,114 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
"""Secure the ports Docker publishes, with rules in the DOCKER-USER chain.
|
||||||
|
|
||||||
|
Usage: docker-rules apply|remove
|
||||||
|
|
||||||
|
Docker evaluates DOCKER-USER in FORWARD before its own rules, which makes it the
|
||||||
|
place to filter traffic to containers. The rules live in their own chain,
|
||||||
|
VPN-ROUTER-DOCKER, entered by one jump at the top of DOCKER-USER, so anything
|
||||||
|
else in DOCKER-USER is left alone. Traffic arriving on the external interface is
|
||||||
|
accepted when it is IPsec protected, when it is TCP to docker.exposed_ports, or
|
||||||
|
when it belongs to an established connection; all other traffic from the
|
||||||
|
external interface is dropped. Traffic from other interfaces is not affected.
|
||||||
|
|
||||||
|
docker.mode decides whether this runs: auto applies the rules when the Docker
|
||||||
|
service is installed, on requires it, off removes the rules. The vpn-router-docker
|
||||||
|
unit runs apply after docker.service starts and remove when it stops.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
|
||||||
|
|
||||||
|
import vpnrouter
|
||||||
|
from vpnrouter import fail, warn
|
||||||
|
|
||||||
|
CHAIN = 'VPN-ROUTER-DOCKER'
|
||||||
|
USER_CHAIN = 'DOCKER-USER'
|
||||||
|
|
||||||
|
|
||||||
|
def iptables(*args, check=True):
|
||||||
|
result = subprocess.run(['iptables', *args], capture_output=True, text=True)
|
||||||
|
if check and result.returncode != 0:
|
||||||
|
fail(f'iptables {" ".join(args)} failed: {result.stderr.strip()}')
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def docker_installed():
|
||||||
|
"""True when systemd knows the docker.service unit."""
|
||||||
|
result = subprocess.run(
|
||||||
|
['systemctl', 'list-unit-files', '--no-legend', 'docker.service'],
|
||||||
|
capture_output=True, text=True)
|
||||||
|
return result.returncode == 0 and result.stdout.strip() != ''
|
||||||
|
|
||||||
|
|
||||||
|
def remove():
|
||||||
|
"""Take the jump and the chain out; nothing happens when they are absent."""
|
||||||
|
while iptables('-D', USER_CHAIN, '-j', CHAIN, check=False).returncode == 0:
|
||||||
|
pass
|
||||||
|
iptables('-F', CHAIN, check=False)
|
||||||
|
iptables('-X', CHAIN, check=False)
|
||||||
|
|
||||||
|
|
||||||
|
def apply(cfg):
|
||||||
|
mode = cfg['docker']['mode']
|
||||||
|
if mode == 'off':
|
||||||
|
remove()
|
||||||
|
return
|
||||||
|
|
||||||
|
if not docker_installed():
|
||||||
|
if mode == 'on':
|
||||||
|
fail('docker.mode is on but the docker service is not installed; '
|
||||||
|
'install Docker before vpn-router')
|
||||||
|
return
|
||||||
|
|
||||||
|
if iptables('-S', USER_CHAIN, check=False).returncode != 0:
|
||||||
|
if mode == 'on':
|
||||||
|
fail(f'docker.mode is on but there is no {USER_CHAIN} chain; '
|
||||||
|
'is the Docker daemon running?')
|
||||||
|
warn(f'no {USER_CHAIN} chain, the Docker daemon is not running; '
|
||||||
|
'the rules are applied when it starts')
|
||||||
|
return
|
||||||
|
|
||||||
|
ctx = vpnrouter.build_context(cfg)
|
||||||
|
wan = ctx['wan_iface']
|
||||||
|
if not wan:
|
||||||
|
fail('the external interface is not known; set interfaces.external '
|
||||||
|
'or use mode auto or interfaces')
|
||||||
|
|
||||||
|
ports = ','.join(item.strip() for item in cfg['docker']['exposed_ports'])
|
||||||
|
|
||||||
|
remove()
|
||||||
|
iptables('-N', CHAIN)
|
||||||
|
iptables('-A', CHAIN, '-i', wan, '-m', 'policy', '--dir', 'in', '--pol', 'ipsec', '-j', 'ACCEPT')
|
||||||
|
if ports:
|
||||||
|
iptables('-A', CHAIN, '-i', wan, '-p', 'tcp', '-m', 'multiport', '--dports', ports, '-j', 'ACCEPT')
|
||||||
|
iptables('-A', CHAIN, '-i', wan, '-m', 'conntrack', '--ctstate', 'ESTABLISHED,RELATED', '-j', 'ACCEPT')
|
||||||
|
iptables('-A', CHAIN, '-i', wan, '-j', 'DROP')
|
||||||
|
iptables('-I', USER_CHAIN, '1', '-j', CHAIN)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) != 2 or sys.argv[1] not in ('apply', 'remove'):
|
||||||
|
print('usage: docker-rules apply|remove', file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
if sys.argv[1] == 'remove':
|
||||||
|
remove()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
cfg = vpnrouter.load_config()
|
||||||
|
if vpnrouter.deferred(cfg):
|
||||||
|
return 0
|
||||||
|
try:
|
||||||
|
vpnrouter.validate(cfg)
|
||||||
|
apply(cfg)
|
||||||
|
except vpnrouter.ConfigError as exc:
|
||||||
|
fail(str(exc))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
"""Write a candidate vpn-router.conf to the path given on the command line.
|
"""Write a candidate vpn-router.json to the path given on the command line.
|
||||||
|
|
||||||
Called from postinst, which hands the result to ucf(1). ucf compares it with
|
Called from postinst, which hands the result to ucf(1). ucf compares it with
|
||||||
/etc/vpn-router/vpn-router.conf and decides what to do about local changes, so
|
/etc/vpn-router/vpn-router.json and decides what to do about local changes, so
|
||||||
this script never writes to /etc and never has to worry about destroying an
|
this script never writes to /etc and never has to worry about destroying an
|
||||||
administrator's edits.
|
administrator's edits.
|
||||||
|
|
||||||
The candidate is built in three layers, each overriding the one before:
|
The candidate is built in three layers, each overriding the one before:
|
||||||
|
|
||||||
1. the shipped defaults
|
1. the shipped defaults
|
||||||
2. the current contents of /etc/vpn-router/vpn-router.conf, if it exists
|
2. the current contents of /etc/vpn-router/vpn-router.json, if it exists
|
||||||
3. any debconf answer that was actually given
|
3. any debconf answer that was actually given
|
||||||
|
|
||||||
Layer 2 is what makes reconfiguration safe: a setting the administrator edited
|
Layer 2 is what makes reconfiguration safe: a setting the administrator edited
|
||||||
@@ -19,12 +19,12 @@ answer means "leave alone", which is why clearing the pre-shared key from the
|
|||||||
debconf database after installation does not blank it on the next
|
debconf database after installation does not blank it on the next
|
||||||
dpkg-reconfigure.
|
dpkg-reconfigure.
|
||||||
|
|
||||||
Answers arrive as VPN_ROUTER_* environment variables. The pre-shared key is
|
Answers arrive as VPN_ROUTER_* environment variables and are converted to the
|
||||||
stored base64-encoded so that arbitrary characters cannot collide with INI
|
type of the setting they fill: comma-separated lists become arrays, "true" and
|
||||||
syntax.
|
"false" become booleans and digits become integers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import base64
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -46,6 +46,7 @@ ANSWERS = {
|
|||||||
'VPN_ROUTER_REMOTE_ADDRS': ('remote', 'addrs'),
|
'VPN_ROUTER_REMOTE_ADDRS': ('remote', 'addrs'),
|
||||||
'VPN_ROUTER_REMOTE_ID': ('remote', 'id'),
|
'VPN_ROUTER_REMOTE_ID': ('remote', 'id'),
|
||||||
'VPN_ROUTER_REMOTE_CIDRS': ('remote', 'cidrs'),
|
'VPN_ROUTER_REMOTE_CIDRS': ('remote', 'cidrs'),
|
||||||
|
'VPN_ROUTER_PSK': ('remote', 'psk'),
|
||||||
'VPN_ROUTER_P2S_ENABLED': ('p2s', 'enabled'),
|
'VPN_ROUTER_P2S_ENABLED': ('p2s', 'enabled'),
|
||||||
'VPN_ROUTER_P2S_ADDRESS_POOL': ('p2s', 'address_pool'),
|
'VPN_ROUTER_P2S_ADDRESS_POOL': ('p2s', 'address_pool'),
|
||||||
'VPN_ROUTER_P2S_CA_NAME': ('p2s', 'ca_name'),
|
'VPN_ROUTER_P2S_CA_NAME': ('p2s', 'ca_name'),
|
||||||
@@ -61,25 +62,25 @@ def main():
|
|||||||
return 2
|
return 2
|
||||||
output = sys.argv[1]
|
output = sys.argv[1]
|
||||||
|
|
||||||
parser = vpnrouter.default_parser()
|
cfg = vpnrouter.load_config()
|
||||||
|
|
||||||
current = vpnrouter.load_config()
|
|
||||||
for section in parser.sections():
|
|
||||||
for option in parser[section]:
|
|
||||||
if current.has_option(section, option):
|
|
||||||
parser[section][option] = current.get(section, option)
|
|
||||||
|
|
||||||
for variable, (section, option) in ANSWERS.items():
|
for variable, (section, option) in ANSWERS.items():
|
||||||
if os.environ.get(variable):
|
value = os.environ.get(variable)
|
||||||
parser[section][option] = os.environ[variable]
|
if not value:
|
||||||
|
continue
|
||||||
psk = os.environ.get('VPN_ROUTER_PSK', '')
|
# A value that does not convert is kept as a string for validate() to report.
|
||||||
if psk:
|
default = vpnrouter.SCHEMA[section][option]
|
||||||
parser['remote']['psk_b64'] = base64.b64encode(
|
if isinstance(default, bool):
|
||||||
psk.encode('utf-8')).decode('ascii')
|
value = value == 'true'
|
||||||
|
elif isinstance(default, int):
|
||||||
|
value = int(value) if value.isdigit() else value
|
||||||
|
elif isinstance(default, list):
|
||||||
|
value = [item.strip() for item in value.split(',') if item.strip()]
|
||||||
|
cfg[section][option] = value
|
||||||
|
|
||||||
with open(output, 'w', encoding='utf-8') as handle:
|
with open(output, 'w', encoding='utf-8') as handle:
|
||||||
parser.write(handle)
|
json.dump(cfg, handle, indent=2)
|
||||||
|
handle.write('\n')
|
||||||
os.chmod(output, 0o600)
|
os.chmod(output, 0o600)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|||||||
@@ -139,10 +139,10 @@ def filter_block(ctx):
|
|||||||
'# WIREGUARD RULES END',
|
'# WIREGUARD RULES END',
|
||||||
]
|
]
|
||||||
|
|
||||||
if vpnrouter.p2s_ready(ctx) and ctx['local_addrs']:
|
if vpnrouter.p2s_ready(ctx) and ctx['local_addr_list']:
|
||||||
pool = ctx['p2s_address_pool']
|
pool = ctx['p2s_address_pool']
|
||||||
lines += ['', '# P2S DNS RULES START']
|
lines += ['', '# P2S DNS RULES START']
|
||||||
for address in [a.strip() for a in ctx['local_addrs'].split(',') if a.strip()]:
|
for address in ctx['local_addr_list']:
|
||||||
lines += [
|
lines += [
|
||||||
f'-A ufw-before-input -s {pool} -d {address} -p udp --dport 53 -j ACCEPT',
|
f'-A ufw-before-input -s {pool} -d {address} -p udp --dport 53 -j ACCEPT',
|
||||||
f'-A ufw-before-input -s {pool} -d {address} -p tcp --dport 53 -j ACCEPT',
|
f'-A ufw-before-input -s {pool} -d {address} -p tcp --dport 53 -j ACCEPT',
|
||||||
@@ -166,7 +166,7 @@ def nat_and_mangle_block(ctx):
|
|||||||
|
|
||||||
if vpnrouter.routing_ready(ctx):
|
if vpnrouter.routing_ready(ctx):
|
||||||
subnets = ctx['local_cidr_list']
|
subnets = ctx['local_cidr_list']
|
||||||
remote = [c.strip() for c in ctx['remote_cidrs'].split(',') if c.strip()]
|
remote = ctx['remote_cidr_list']
|
||||||
lines += [
|
lines += [
|
||||||
'',
|
'',
|
||||||
'# ROUTER NAT RULES START',
|
'# ROUTER NAT RULES START',
|
||||||
@@ -299,13 +299,23 @@ def run_platform_setup(cfg, ctx):
|
|||||||
fail(f'platform module {module.__name__} failed: {exc}')
|
fail(f'platform module {module.__name__} failed: {exc}')
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_docker_rules():
|
||||||
|
"""Apply the new settings to the Docker rules while Docker is running.
|
||||||
|
|
||||||
|
When Docker is not running, vpn-router-docker applies them after
|
||||||
|
docker.service starts.
|
||||||
|
"""
|
||||||
|
if unit_is_active('docker.service'):
|
||||||
|
run(['systemctl', 'restart', 'vpn-router-docker.service'], check=False)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
cfg = vpnrouter.load_config()
|
cfg = vpnrouter.load_config()
|
||||||
ctx, changed = vpnrouter.configure(cfg)
|
ctx, changed = vpnrouter.configure(cfg)
|
||||||
|
|
||||||
if ctx is None:
|
if ctx is None:
|
||||||
print('vpn-router: platform is none, configuration deferred; '
|
print('vpn-router: platform is none, configuration deferred; '
|
||||||
'set general.platform in /etc/vpn-router/vpn-router.conf')
|
f'set general.platform in {vpnrouter.CONFIG_FILE}')
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
if vpnrouter.routing_ready(ctx):
|
if vpnrouter.routing_ready(ctx):
|
||||||
@@ -337,6 +347,7 @@ def main():
|
|||||||
run(['systemctl', 'restart', 'systemd-resolved'], check=False)
|
run(['systemctl', 'restart', 'systemd-resolved'], check=False)
|
||||||
|
|
||||||
run_platform_setup(cfg, ctx)
|
run_platform_setup(cfg, ctx)
|
||||||
|
refresh_docker_rules()
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,11 @@ Everything derivable from the configuration file alone, with no running
|
|||||||
network required. Used by both 'configure' and 'setup'.
|
network required. Used by both 'configure' and 'setup'.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import base64
|
|
||||||
import binascii
|
|
||||||
import configparser
|
|
||||||
import ipaddress
|
import ipaddress
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
@@ -22,7 +21,7 @@ if str(LIB_DIR) not in sys.path:
|
|||||||
|
|
||||||
import vpnrouter_platforms
|
import vpnrouter_platforms
|
||||||
|
|
||||||
CONFIG_FILE = pathlib.Path('/etc/vpn-router/vpn-router.conf')
|
CONFIG_FILE = pathlib.Path('/etc/vpn-router/vpn-router.json')
|
||||||
TEMPLATE_DIR = pathlib.Path('/usr/share/vpn-router/templates')
|
TEMPLATE_DIR = pathlib.Path('/usr/share/vpn-router/templates')
|
||||||
PKI_DIR = pathlib.Path('/etc/vpn-router/pki')
|
PKI_DIR = pathlib.Path('/etc/vpn-router/pki')
|
||||||
SIMPLE_CA = LIB_DIR / 'simple-ca'
|
SIMPLE_CA = LIB_DIR / 'simple-ca'
|
||||||
@@ -39,17 +38,29 @@ SERVER_KEY = pathlib.Path('/etc/swanctl/private/server.key')
|
|||||||
CRL_DEST = pathlib.Path('/etc/swanctl/x509crl/crl.pem')
|
CRL_DEST = pathlib.Path('/etc/swanctl/x509crl/crl.pem')
|
||||||
TRUST_ANCHOR = pathlib.Path('/usr/local/share/ca-certificates/vpn-router-ca.crt')
|
TRUST_ANCHOR = pathlib.Path('/usr/local/share/ca-certificates/vpn-router-ca.crt')
|
||||||
|
|
||||||
# section -> option -> default
|
# section -> option -> default; the type of the default is the required type.
|
||||||
SCHEMA = {
|
SCHEMA = {
|
||||||
'general': {'platform': 'none', 'mode': 'manual'},
|
'general': {'platform': 'none', 'mode': 'manual'},
|
||||||
'interfaces': {'external': '', 'internal': ''},
|
'interfaces': {'external': '', 'internal': ''},
|
||||||
'wan': {'local_fqdn': '', 'local_id_mode': 'fqdn'},
|
'wan': {'local_fqdn': '', 'local_id_mode': 'fqdn'},
|
||||||
'local': {'cidrs': '', 'int_addr': '', 'int_gateway_ip': ''},
|
'local': {'cidrs': [], 'int_addr': '', 'int_gateway_ip': ''},
|
||||||
'remote': {'addrs': '', 'id': '', 'cidrs': '', 'psk_b64': '', 'psk_file': ''},
|
'remote': {'addrs': [], 'id': '', 'cidrs': [], 'psk': '', 'psk_file': ''},
|
||||||
'p2s': {'enabled': 'false', 'address_pool': '', 'ca_name': 'VPN Router CA'},
|
'p2s': {'enabled': False, 'address_pool': '', 'ca_name': 'VPN Router CA'},
|
||||||
'wireguard': {'enabled': 'false', 'address': '', 'listen_port': '51820'},
|
'wireguard': {'enabled': False, 'address': '', 'listen_port': 51820},
|
||||||
|
'docker': {'mode': 'auto', 'exposed_ports': []},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DOCKER_MODES = ('auto', 'on', 'off')
|
||||||
|
|
||||||
|
#: A port or a port range as iptables multiport takes it: 443 or 8000:8100.
|
||||||
|
DOCKER_PORT = re.compile(r'^\d{1,5}(:\d{1,5})?$')
|
||||||
|
|
||||||
|
#: The multiport match takes at most 15 ports, and a range counts as two.
|
||||||
|
DOCKER_PORT_LIMIT = 15
|
||||||
|
|
||||||
|
TYPE_NAMES = {str: 'a string', list: 'a list of strings', bool: 'a boolean',
|
||||||
|
int: 'an integer'}
|
||||||
|
|
||||||
ID_MODES = ('fqdn', 'public_ip', 'internal_ip')
|
ID_MODES = ('fqdn', 'public_ip', 'internal_ip')
|
||||||
|
|
||||||
#: How much the operator supplies, and therefore how much may be detected.
|
#: How much the operator supplies, and therefore how much may be detected.
|
||||||
@@ -77,46 +88,29 @@ def fail(message):
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def new_parser():
|
|
||||||
"""A parser that tolerates operator comments and never interpolates.
|
|
||||||
|
|
||||||
interpolation=None matters: the default BasicInterpolation treats '%' as
|
|
||||||
syntax, which would break any value containing it.
|
|
||||||
|
|
||||||
Inline comments are enabled, which configparser leaves off by default
|
|
||||||
because it makes '#' and ';' unusable inside values. That is an acceptable
|
|
||||||
trade here precisely because the one setting that can hold arbitrary
|
|
||||||
characters, the pre-shared key, is base64-encoded, and base64 uses none of
|
|
||||||
those characters.
|
|
||||||
"""
|
|
||||||
return configparser.ConfigParser(
|
|
||||||
interpolation=None,
|
|
||||||
comment_prefixes=('#', ';'),
|
|
||||||
inline_comment_prefixes=('#', ';'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def default_parser():
|
|
||||||
parser = new_parser()
|
|
||||||
for section, options in SCHEMA.items():
|
|
||||||
parser[section] = dict(options)
|
|
||||||
return parser
|
|
||||||
|
|
||||||
|
|
||||||
def load_config(path=None):
|
def load_config(path=None):
|
||||||
"""Read the configuration, filling in defaults for anything absent.
|
"""Read the configuration, filling in defaults for anything absent.
|
||||||
|
|
||||||
|
The result maps section -> option -> value. Sections and options not in
|
||||||
|
SCHEMA are kept so that validate() can report them.
|
||||||
|
|
||||||
The path is resolved when called, not when this function is defined, so
|
The path is resolved when called, not when this function is defined, so
|
||||||
CONFIG_FILE stays overridable.
|
CONFIG_FILE stays overridable.
|
||||||
"""
|
"""
|
||||||
path = pathlib.Path(path) if path else CONFIG_FILE
|
path = pathlib.Path(path) if path else CONFIG_FILE
|
||||||
parser = default_parser()
|
cfg = {section: dict(options) for section, options in SCHEMA.items()}
|
||||||
if path.exists():
|
if path.exists():
|
||||||
try:
|
try:
|
||||||
parser.read(path, encoding='utf-8')
|
data = json.loads(path.read_text(encoding='utf-8'))
|
||||||
except configparser.Error as exc:
|
except ValueError as exc:
|
||||||
fail(f'cannot parse {path}: {exc}')
|
fail(f'cannot parse {path}: {exc}')
|
||||||
return parser
|
if not isinstance(data, dict):
|
||||||
|
fail(f'cannot parse {path}: the top level is not an object')
|
||||||
|
for section, options in data.items():
|
||||||
|
if not isinstance(options, dict):
|
||||||
|
fail(f'cannot parse {path}: {section} is not an object')
|
||||||
|
cfg.setdefault(section, {}).update(options)
|
||||||
|
return cfg
|
||||||
|
|
||||||
|
|
||||||
def _split_list(value):
|
def _split_list(value):
|
||||||
@@ -124,19 +118,31 @@ def _split_list(value):
|
|||||||
|
|
||||||
|
|
||||||
def _get(cfg, section, option):
|
def _get(cfg, section, option):
|
||||||
return cfg.get(section, option, fallback=SCHEMA[section][option]).strip()
|
value = cfg[section][option]
|
||||||
|
return value.strip() if isinstance(value, str) else value
|
||||||
|
|
||||||
|
|
||||||
def _get_bool(cfg, section, option):
|
def _get_list(cfg, section, option):
|
||||||
try:
|
return [item.strip() for item in cfg[section][option] if item.strip()]
|
||||||
return cfg.getboolean(section, option,
|
|
||||||
fallback=SCHEMA[section][option] == 'true')
|
|
||||||
except ValueError:
|
|
||||||
raise ConfigError(f'{section}.{option} is not a boolean')
|
|
||||||
|
|
||||||
|
|
||||||
def _check_networks(value, name):
|
def _check_types(cfg):
|
||||||
for item in _split_list(value):
|
for section, options in cfg.items():
|
||||||
|
if section not in SCHEMA:
|
||||||
|
raise ConfigError(f'{section}: unknown section')
|
||||||
|
for option, value in options.items():
|
||||||
|
if option not in SCHEMA[section]:
|
||||||
|
raise ConfigError(f'{section}.{option}: unknown setting')
|
||||||
|
expected = type(SCHEMA[section][option])
|
||||||
|
if type(value) is not expected or (
|
||||||
|
expected is list and
|
||||||
|
not all(isinstance(item, str) for item in value)):
|
||||||
|
raise ConfigError(
|
||||||
|
f'{section}.{option} must be {TYPE_NAMES[expected]}')
|
||||||
|
|
||||||
|
|
||||||
|
def _check_networks(values, name):
|
||||||
|
for item in values:
|
||||||
try:
|
try:
|
||||||
ipaddress.ip_network(item, strict=False)
|
ipaddress.ip_network(item, strict=False)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@@ -158,6 +164,8 @@ def validate(cfg):
|
|||||||
The exception is general.mode, which states what the operator promised to
|
The exception is general.mode, which states what the operator promised to
|
||||||
supply, so the values that mode requires are checked for presence too.
|
supply, so the values that mode requires are checked for presence too.
|
||||||
"""
|
"""
|
||||||
|
_check_types(cfg)
|
||||||
|
|
||||||
platform = _get(cfg, 'general', 'platform')
|
platform = _get(cfg, 'general', 'platform')
|
||||||
installed = [PLATFORM_NONE] + vpnrouter_platforms.available()
|
installed = [PLATFORM_NONE] + vpnrouter_platforms.available()
|
||||||
if platform and platform not in installed:
|
if platform and platform not in installed:
|
||||||
@@ -187,9 +195,10 @@ def validate(cfg):
|
|||||||
raise ConfigError(
|
raise ConfigError(
|
||||||
f'wan.local_id_mode: {id_mode!r} is not one of {", ".join(ID_MODES)}')
|
f'wan.local_id_mode: {id_mode!r} is not one of {", ".join(ID_MODES)}')
|
||||||
|
|
||||||
_check_networks(_get(cfg, 'local', 'cidrs'), 'local.cidrs')
|
_check_networks(_get_list(cfg, 'local', 'cidrs'), 'local.cidrs')
|
||||||
_check_networks(_get(cfg, 'remote', 'cidrs'), 'remote.cidrs')
|
_check_networks(_get_list(cfg, 'remote', 'cidrs'), 'remote.cidrs')
|
||||||
_check_networks(_get(cfg, 'p2s', 'address_pool'), 'p2s.address_pool')
|
pool = _get(cfg, 'p2s', 'address_pool')
|
||||||
|
_check_networks([pool] if pool else [], 'p2s.address_pool')
|
||||||
_check_address(_get(cfg, 'local', 'int_addr'), 'local.int_addr')
|
_check_address(_get(cfg, 'local', 'int_addr'), 'local.int_addr')
|
||||||
_check_address(_get(cfg, 'local', 'int_gateway_ip'), 'local.int_gateway_ip')
|
_check_address(_get(cfg, 'local', 'int_gateway_ip'), 'local.int_gateway_ip')
|
||||||
|
|
||||||
@@ -210,12 +219,26 @@ def validate(cfg):
|
|||||||
f'wireguard.address: {wg_address!r} is not an address with prefix')
|
f'wireguard.address: {wg_address!r} is not an address with prefix')
|
||||||
|
|
||||||
port = _get(cfg, 'wireguard', 'listen_port')
|
port = _get(cfg, 'wireguard', 'listen_port')
|
||||||
if port:
|
if not 1 <= port <= 65535:
|
||||||
if not port.isdigit() or not 1 <= int(port) <= 65535:
|
raise ConfigError(f'wireguard.listen_port: {port} is not a valid port')
|
||||||
raise ConfigError(f'wireguard.listen_port: {port!r} is not a valid port')
|
|
||||||
|
|
||||||
for section in ('p2s', 'wireguard'):
|
docker_mode = _get(cfg, 'docker', 'mode')
|
||||||
_get_bool(cfg, section, 'enabled')
|
if docker_mode not in DOCKER_MODES:
|
||||||
|
raise ConfigError(
|
||||||
|
f'docker.mode: {docker_mode!r} is not one of {", ".join(DOCKER_MODES)}')
|
||||||
|
|
||||||
|
ports = _get_list(cfg, 'docker', 'exposed_ports')
|
||||||
|
weight = 0
|
||||||
|
for item in ports:
|
||||||
|
if not DOCKER_PORT.match(item) or any(
|
||||||
|
not 1 <= int(part) <= 65535 for part in item.split(':')):
|
||||||
|
raise ConfigError(
|
||||||
|
f'docker.exposed_ports: {item!r} is not a port or a port range')
|
||||||
|
weight += 2 if ':' in item else 1
|
||||||
|
if weight > DOCKER_PORT_LIMIT:
|
||||||
|
raise ConfigError(
|
||||||
|
f'docker.exposed_ports: at most {DOCKER_PORT_LIMIT} ports are allowed, '
|
||||||
|
'a range counting as two')
|
||||||
|
|
||||||
|
|
||||||
def resolve_psk(cfg):
|
def resolve_psk(cfg):
|
||||||
@@ -226,14 +249,7 @@ def resolve_psk(cfg):
|
|||||||
if not path.is_file():
|
if not path.is_file():
|
||||||
raise ConfigError(f'remote.psk_file: {psk_file} does not exist')
|
raise ConfigError(f'remote.psk_file: {psk_file} does not exist')
|
||||||
return path.read_text(encoding='utf-8').strip()
|
return path.read_text(encoding='utf-8').strip()
|
||||||
|
return cfg['remote']['psk']
|
||||||
psk_b64 = _get(cfg, 'remote', 'psk_b64')
|
|
||||||
if not psk_b64:
|
|
||||||
return ''
|
|
||||||
try:
|
|
||||||
return base64.b64decode(psk_b64, validate=True).decode('utf-8')
|
|
||||||
except (binascii.Error, UnicodeDecodeError) as exc:
|
|
||||||
raise ConfigError(f'remote.psk_b64 is not valid base64 text: {exc}')
|
|
||||||
|
|
||||||
|
|
||||||
def platform_module(cfg):
|
def platform_module(cfg):
|
||||||
@@ -253,7 +269,7 @@ def platform_module(cfg):
|
|||||||
def _join(*groups):
|
def _join(*groups):
|
||||||
items = []
|
items = []
|
||||||
for group in groups:
|
for group in groups:
|
||||||
for item in _split_list(group):
|
for item in group:
|
||||||
if item not in items:
|
if item not in items:
|
||||||
items.append(item)
|
items.append(item)
|
||||||
return ', '.join(items)
|
return ', '.join(items)
|
||||||
@@ -293,12 +309,22 @@ def detect_interfaces():
|
|||||||
return external, candidates[0]
|
return external, candidates[0]
|
||||||
|
|
||||||
|
|
||||||
|
def _is_virtual(name):
|
||||||
|
"""True for an interface the kernel creates without a device behind it.
|
||||||
|
|
||||||
|
Loopback, bridges, veth pairs and tunnels live under /sys/devices/virtual,
|
||||||
|
which is where the Docker bridge is; a NIC of the virtual machine does not.
|
||||||
|
"""
|
||||||
|
link = pathlib.Path('/sys/class/net') / name
|
||||||
|
return '/devices/virtual/' in str(link.resolve())
|
||||||
|
|
||||||
|
|
||||||
def _addressed_interfaces():
|
def _addressed_interfaces():
|
||||||
"""Interface names holding at least one IPv4 address, excluding loopback."""
|
"""Names of the NICs holding at least one IPv4 address; virtual interfaces are left out."""
|
||||||
names = []
|
names = []
|
||||||
for line in _ip_route('-o', '-4', 'addr', 'show').splitlines():
|
for line in _ip_route('-o', '-4', 'addr', 'show').splitlines():
|
||||||
fields = line.split()
|
fields = line.split()
|
||||||
if len(fields) >= 4 and fields[1] != 'lo' and fields[1] not in names:
|
if len(fields) >= 4 and fields[1] not in names and not _is_virtual(fields[1]):
|
||||||
names.append(fields[1])
|
names.append(fields[1])
|
||||||
return names
|
return names
|
||||||
|
|
||||||
@@ -369,9 +395,8 @@ def resolve_interfaces(cfg):
|
|||||||
if mode == 'auto':
|
if mode == 'auto':
|
||||||
ext_iface, int_iface = detect_interfaces()
|
ext_iface, int_iface = detect_interfaces()
|
||||||
|
|
||||||
ext_addrs = interface_addresses(ext_iface, 'interfaces.external')
|
local_addrs = interface_addresses(ext_iface, 'interfaces.external')
|
||||||
local_addrs = ', '.join(ext_addrs)
|
if ext_iface and not local_addrs:
|
||||||
if ext_iface and not ext_addrs:
|
|
||||||
warn(f'{ext_iface} has no IPv4 address yet')
|
warn(f'{ext_iface} has no IPv4 address yet')
|
||||||
|
|
||||||
if mode == 'manual':
|
if mode == 'manual':
|
||||||
@@ -393,7 +418,8 @@ def build_context(cfg, local_id_override=None):
|
|||||||
|
|
||||||
local_fqdn = _get(cfg, 'wan', 'local_fqdn')
|
local_fqdn = _get(cfg, 'wan', 'local_fqdn')
|
||||||
id_mode = _get(cfg, 'wan', 'local_id_mode') or 'fqdn'
|
id_mode = _get(cfg, 'wan', 'local_id_mode') or 'fqdn'
|
||||||
local_cidrs = _get(cfg, 'local', 'cidrs')
|
local_cidrs = _get_list(cfg, 'local', 'cidrs')
|
||||||
|
remote_cidrs = _get_list(cfg, 'remote', 'cidrs')
|
||||||
|
|
||||||
if local_id_override:
|
if local_id_override:
|
||||||
local_id = local_id_override
|
local_id = local_id_override
|
||||||
@@ -406,32 +432,31 @@ def build_context(cfg, local_id_override=None):
|
|||||||
# re-renders with local_id_override set.
|
# re-renders with local_id_override set.
|
||||||
local_id = ''
|
local_id = ''
|
||||||
|
|
||||||
cidr_list = _split_list(local_cidrs)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'platform': _get(cfg, 'general', 'platform') or PLATFORM_NONE,
|
'platform': _get(cfg, 'general', 'platform') or PLATFORM_NONE,
|
||||||
'mode': _get(cfg, 'general', 'mode') or 'manual',
|
'mode': _get(cfg, 'general', 'mode') or 'manual',
|
||||||
'wan_iface': ext_iface,
|
'wan_iface': ext_iface,
|
||||||
'int_iface': int_iface,
|
'int_iface': int_iface,
|
||||||
'local_addrs': local_addrs,
|
'local_addrs': ', '.join(local_addrs),
|
||||||
'local_addr_list': _split_list(local_addrs),
|
'local_addr_list': local_addrs,
|
||||||
'local_fqdn': local_fqdn,
|
'local_fqdn': local_fqdn,
|
||||||
'local_id_mode': id_mode,
|
'local_id_mode': id_mode,
|
||||||
'local_id': local_id,
|
'local_id': local_id,
|
||||||
'local_cidrs': local_cidrs,
|
'local_cidrs': ', '.join(local_cidrs),
|
||||||
'local_cidr_list': cidr_list,
|
'local_cidr_list': local_cidrs,
|
||||||
'local_ts': _join(local_cidrs, extras['EXTRA_LOCAL_TS']),
|
'local_ts': _join(local_cidrs, _split_list(extras['EXTRA_LOCAL_TS'])),
|
||||||
'int_addr': int_addr,
|
'int_addr': int_addr,
|
||||||
'int_gateway_ip': gateway,
|
'int_gateway_ip': gateway,
|
||||||
'remote_addrs': _get(cfg, 'remote', 'addrs'),
|
'remote_addrs': ', '.join(_get_list(cfg, 'remote', 'addrs')),
|
||||||
'remote_id': _get(cfg, 'remote', 'id'),
|
'remote_id': _get(cfg, 'remote', 'id'),
|
||||||
'remote_cidrs': _get(cfg, 'remote', 'cidrs'),
|
'remote_cidrs': ', '.join(remote_cidrs),
|
||||||
|
'remote_cidr_list': remote_cidrs,
|
||||||
'psk': resolve_psk(cfg),
|
'psk': resolve_psk(cfg),
|
||||||
'p2s_enabled': _get_bool(cfg, 'p2s', 'enabled'),
|
'p2s_enabled': _get(cfg, 'p2s', 'enabled'),
|
||||||
'p2s_address_pool': _get(cfg, 'p2s', 'address_pool'),
|
'p2s_address_pool': _get(cfg, 'p2s', 'address_pool'),
|
||||||
'p2s_ca_name': _get(cfg, 'p2s', 'ca_name'),
|
'p2s_ca_name': _get(cfg, 'p2s', 'ca_name'),
|
||||||
'p2s_dns': _join(local_addrs, extras['EXTRA_P2S_DNS']),
|
'p2s_dns': _join(local_addrs, _split_list(extras['EXTRA_P2S_DNS'])),
|
||||||
'wg_enabled': _get_bool(cfg, 'wireguard', 'enabled'),
|
'wg_enabled': _get(cfg, 'wireguard', 'enabled'),
|
||||||
'wg_address': _get(cfg, 'wireguard', 'address'),
|
'wg_address': _get(cfg, 'wireguard', 'address'),
|
||||||
'wg_listen_port': _get(cfg, 'wireguard', 'listen_port'),
|
'wg_listen_port': _get(cfg, 'wireguard', 'listen_port'),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
# Example vpn-router configuration.
|
|
||||||
#
|
|
||||||
# This file is documentation. The live configuration is
|
|
||||||
# /etc/vpn-router/vpn-router.conf, which is generated on first install and
|
|
||||||
# owned by the operator afterwards. Copy settings from here as needed.
|
|
||||||
#
|
|
||||||
# Format: INI. Lists are comma-separated, booleans are true/false, an empty
|
|
||||||
# value means unset, and a _b64 suffix means the value is base64-encoded.
|
|
||||||
#
|
|
||||||
# Comments start with # or ; and may follow a value on the same line. That
|
|
||||||
# means # and ; cannot appear inside a value - which is why the pre-shared
|
|
||||||
# key, the one setting that can hold arbitrary text, is base64-encoded.
|
|
||||||
#
|
|
||||||
# Apply any change with: systemctl restart vpn-router-setup
|
|
||||||
#
|
|
||||||
# Every feature is optional. A file with nothing filled in configures nothing
|
|
||||||
# and leaves the host reachable and unchanged.
|
|
||||||
|
|
||||||
[general]
|
|
||||||
# Platform: none, generic, azure or gcp.
|
|
||||||
# none - configuration is deferred. Files are installed and nothing on
|
|
||||||
# this machine is changed. Set a real platform when ready.
|
|
||||||
# generic - configure, with no platform-specific additions.
|
|
||||||
platform = none
|
|
||||||
|
|
||||||
# How much of the network configuration you supply, and therefore how much is
|
|
||||||
# read from the system:
|
|
||||||
# manual - interface names, int_addr and int_gateway_ip are all given
|
|
||||||
# below. Nothing is detected.
|
|
||||||
# interfaces - the two interface names are given; int_addr and
|
|
||||||
# int_gateway_ip are read from them.
|
|
||||||
# auto - nothing is given. The interfaces are identified from the
|
|
||||||
# routing table. Convenient, and able to get it wrong.
|
|
||||||
mode = manual
|
|
||||||
|
|
||||||
[interfaces]
|
|
||||||
# External faces the untrusted network; internal faces the protected one.
|
|
||||||
# Required in manual and interfaces mode; worked out for you in auto mode.
|
|
||||||
external =
|
|
||||||
internal =
|
|
||||||
|
|
||||||
[wan]
|
|
||||||
# This router's fully-qualified domain name.
|
|
||||||
local_fqdn =
|
|
||||||
# Source of the local IKE identity: fqdn | public_ip | internal_ip
|
|
||||||
# fqdn - use local_fqdn (must match what the peer expects)
|
|
||||||
# public_ip - resolve local_fqdn at boot and use the address
|
|
||||||
# internal_ip - use the address on the internal interface
|
|
||||||
local_id_mode = fqdn
|
|
||||||
|
|
||||||
[local]
|
|
||||||
# Local subnets advertised into the tunnel, comma-separated.
|
|
||||||
cidrs =
|
|
||||||
# This host's own address on the internal network, and the next hop on that
|
|
||||||
# side for the subnets above. Both are required in manual mode and read from
|
|
||||||
# the internal interface in the other two.
|
|
||||||
int_addr =
|
|
||||||
int_gateway_ip =
|
|
||||||
|
|
||||||
[remote]
|
|
||||||
# Remote gateway address(es) or FQDN, comma-separated.
|
|
||||||
addrs =
|
|
||||||
# Remote IKE identity, without a leading '@'.
|
|
||||||
id =
|
|
||||||
# Remote subnets reachable through the tunnel, comma-separated.
|
|
||||||
cidrs =
|
|
||||||
# Pre-shared key, base64-encoded: printf %s "$PSK" | base64 -w0
|
|
||||||
psk_b64 =
|
|
||||||
# Path to a file containing the raw PSK. Takes precedence over psk_b64.
|
|
||||||
psk_file =
|
|
||||||
|
|
||||||
[p2s]
|
|
||||||
# Road-warrior access.
|
|
||||||
enabled = false
|
|
||||||
# Address pool handed to road-warrior clients.
|
|
||||||
address_pool =
|
|
||||||
# CN for the CA created when /etc/vpn-router/pki is empty.
|
|
||||||
ca_name = VPN Router CA
|
|
||||||
|
|
||||||
[wireguard]
|
|
||||||
enabled = false
|
|
||||||
# Address and prefix length for wg0, for example 192.168.200.1/24.
|
|
||||||
address =
|
|
||||||
listen_port = 51820
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"general": {
|
||||||
|
"platform": "generic",
|
||||||
|
"mode": "manual"
|
||||||
|
},
|
||||||
|
"interfaces": {
|
||||||
|
"external": "eth0",
|
||||||
|
"internal": "eth1"
|
||||||
|
},
|
||||||
|
"wan": {
|
||||||
|
"local_fqdn": "router.example.com",
|
||||||
|
"local_id_mode": "fqdn"
|
||||||
|
},
|
||||||
|
"local": {
|
||||||
|
"cidrs": ["10.0.0.0/24"],
|
||||||
|
"int_addr": "10.1.1.4",
|
||||||
|
"int_gateway_ip": "10.1.1.1"
|
||||||
|
},
|
||||||
|
"remote": {
|
||||||
|
"addrs": ["peer.example.net"],
|
||||||
|
"id": "peer.example.net",
|
||||||
|
"cidrs": ["192.168.0.0/24"],
|
||||||
|
"psk": "change-me",
|
||||||
|
"psk_file": ""
|
||||||
|
},
|
||||||
|
"p2s": {
|
||||||
|
"enabled": false,
|
||||||
|
"address_pool": "172.16.0.0/24",
|
||||||
|
"ca_name": "VPN Router CA"
|
||||||
|
},
|
||||||
|
"wireguard": {
|
||||||
|
"enabled": false,
|
||||||
|
"address": "192.168.200.1/24",
|
||||||
|
"listen_port": 51820
|
||||||
|
},
|
||||||
|
"docker": {
|
||||||
|
"mode": "auto",
|
||||||
|
"exposed_ports": ["443"]
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
-18
@@ -11,24 +11,21 @@ locals {
|
|||||||
repo_url = var.repo_url
|
repo_url = var.repo_url
|
||||||
repo_gpg_key = data.http.repo_gpg_key.response_body
|
repo_gpg_key = data.http.repo_gpg_key.response_body
|
||||||
ubuntu_codename = var.ubuntu_codename
|
ubuntu_codename = var.ubuntu_codename
|
||||||
platform = "azure"
|
|
||||||
mode = "auto"
|
# Contents of /etc/vpn-router/vpn-router.json.
|
||||||
external_interface = ""
|
config = {
|
||||||
internal_interface = ""
|
general = { platform = "azure", mode = "auto" }
|
||||||
local_id_mode = var.local_id_mode
|
wan = { local_fqdn = var.local_fqdn, local_id_mode = var.local_id_mode }
|
||||||
local_cidrs = var.local_cidrs
|
local = { cidrs = [for c in split(",", var.local_cidrs) : trimspace(c) if trimspace(c) != ""] }
|
||||||
int_addr = ""
|
remote = {
|
||||||
int_gateway_ip = ""
|
addrs = [for a in split(",", var.remote_addrs) : trimspace(a) if trimspace(a) != ""]
|
||||||
remote_addrs = var.remote_addrs
|
id = var.remote_id
|
||||||
remote_id = var.remote_id
|
cidrs = local.remote_cidr_list
|
||||||
remote_cidrs = var.remote_cidrs
|
psk = var.psk
|
||||||
psk_b64 = base64encode(var.psk)
|
}
|
||||||
p2s_enabled = var.p2s_enabled
|
p2s = { enabled = var.p2s_enabled, address_pool = var.p2s_address_pool, ca_name = "VPN Router CA" }
|
||||||
p2s_address_pool = var.p2s_address_pool
|
wireguard = { enabled = var.wireguard_enabled, address = var.wireguard_address, listen_port = var.wireguard_listen_port }
|
||||||
p2s_ca_name = "VPN Router CA"
|
}
|
||||||
wg_enabled = var.wireguard_enabled
|
|
||||||
wg_address = var.wireguard_address
|
|
||||||
wg_listen_port = var.wireguard_listen_port
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,42 +22,11 @@ apt:
|
|||||||
write_files:
|
write_files:
|
||||||
# The configuration file. Created here before the package is installed, so
|
# The configuration file. Created here before the package is installed, so
|
||||||
# postinst leaves it alone.
|
# postinst leaves it alone.
|
||||||
- path: /etc/vpn-router/vpn-router.conf
|
- path: /etc/vpn-router/vpn-router.json
|
||||||
permissions: '0600'
|
permissions: '0600'
|
||||||
owner: root:root
|
owner: root:root
|
||||||
content: |
|
content: |
|
||||||
[general]
|
${jsonencode(config)}
|
||||||
platform = ${platform}
|
|
||||||
mode = ${mode}
|
|
||||||
|
|
||||||
[interfaces]
|
|
||||||
external = ${external_interface}
|
|
||||||
internal = ${internal_interface}
|
|
||||||
|
|
||||||
[wan]
|
|
||||||
local_fqdn = ${fqdn}
|
|
||||||
local_id_mode = ${local_id_mode}
|
|
||||||
|
|
||||||
[local]
|
|
||||||
cidrs = ${local_cidrs}
|
|
||||||
int_addr = ${int_addr}
|
|
||||||
int_gateway_ip = ${int_gateway_ip}
|
|
||||||
|
|
||||||
[remote]
|
|
||||||
addrs = ${remote_addrs}
|
|
||||||
id = ${remote_id}
|
|
||||||
cidrs = ${remote_cidrs}
|
|
||||||
psk_b64 = ${psk_b64}
|
|
||||||
|
|
||||||
[p2s]
|
|
||||||
enabled = ${p2s_enabled}
|
|
||||||
address_pool = ${p2s_address_pool}
|
|
||||||
ca_name = ${p2s_ca_name}
|
|
||||||
|
|
||||||
[wireguard]
|
|
||||||
enabled = ${wg_enabled}
|
|
||||||
address = ${wg_address}
|
|
||||||
listen_port = ${wg_listen_port}
|
|
||||||
|
|
||||||
- path: /etc/vpn-router/pki/ca_cert.pem
|
- path: /etc/vpn-router/pki/ca_cert.pem
|
||||||
permissions: '0644'
|
permissions: '0644'
|
||||||
|
|||||||
@@ -28,42 +28,11 @@ apt:
|
|||||||
write_files:
|
write_files:
|
||||||
# The configuration file. Created here before the package is installed, so
|
# The configuration file. Created here before the package is installed, so
|
||||||
# postinst leaves it alone.
|
# postinst leaves it alone.
|
||||||
- path: /etc/vpn-router/vpn-router.conf
|
- path: /etc/vpn-router/vpn-router.json
|
||||||
permissions: '0600'
|
permissions: '0600'
|
||||||
owner: root:root
|
owner: root:root
|
||||||
content: |
|
content: |
|
||||||
[general]
|
${jsonencode(config)}
|
||||||
platform = ${platform}
|
|
||||||
mode = ${mode}
|
|
||||||
|
|
||||||
[interfaces]
|
|
||||||
external = ${external_interface}
|
|
||||||
internal = ${internal_interface}
|
|
||||||
|
|
||||||
[wan]
|
|
||||||
local_fqdn = ${fqdn}
|
|
||||||
local_id_mode = ${local_id_mode}
|
|
||||||
|
|
||||||
[local]
|
|
||||||
cidrs = ${local_cidrs}
|
|
||||||
int_addr = ${int_addr}
|
|
||||||
int_gateway_ip = ${int_gateway_ip}
|
|
||||||
|
|
||||||
[remote]
|
|
||||||
addrs = ${remote_addrs}
|
|
||||||
id = ${remote_id}
|
|
||||||
cidrs = ${remote_cidrs}
|
|
||||||
psk_b64 = ${psk_b64}
|
|
||||||
|
|
||||||
[p2s]
|
|
||||||
enabled = ${p2s_enabled}
|
|
||||||
address_pool = ${p2s_address_pool}
|
|
||||||
ca_name = ${p2s_ca_name}
|
|
||||||
|
|
||||||
[wireguard]
|
|
||||||
enabled = ${wg_enabled}
|
|
||||||
address = ${wg_address}
|
|
||||||
listen_port = ${wg_listen_port}
|
|
||||||
|
|
||||||
package_update: true
|
package_update: true
|
||||||
|
|
||||||
|
|||||||
+15
-18
@@ -11,24 +11,21 @@ locals {
|
|||||||
repo_url = var.repo_url
|
repo_url = var.repo_url
|
||||||
repo_gpg_key = data.http.repo_gpg_key.response_body
|
repo_gpg_key = data.http.repo_gpg_key.response_body
|
||||||
ubuntu_codename = var.ubuntu_codename
|
ubuntu_codename = var.ubuntu_codename
|
||||||
platform = "gcp"
|
|
||||||
mode = "auto"
|
# Contents of /etc/vpn-router/vpn-router.json.
|
||||||
external_interface = ""
|
config = {
|
||||||
internal_interface = ""
|
general = { platform = "gcp", mode = "auto" }
|
||||||
local_id_mode = var.local_id_mode
|
wan = { local_fqdn = var.local_fqdn, local_id_mode = var.local_id_mode }
|
||||||
local_cidrs = var.local_cidrs
|
local = { cidrs = [for c in split(",", var.local_cidrs) : trimspace(c) if trimspace(c) != ""] }
|
||||||
int_addr = ""
|
remote = {
|
||||||
int_gateway_ip = ""
|
addrs = [for a in split(",", var.remote_addrs) : trimspace(a) if trimspace(a) != ""]
|
||||||
remote_addrs = var.remote_addrs
|
id = var.remote_id
|
||||||
remote_id = var.remote_id
|
cidrs = local.remote_cidr_list
|
||||||
remote_cidrs = var.remote_cidrs
|
psk = var.psk
|
||||||
psk_b64 = base64encode(var.psk)
|
}
|
||||||
p2s_enabled = var.p2s_enabled
|
p2s = { enabled = var.p2s_enabled, address_pool = var.p2s_address_pool, ca_name = "VPN Router CA" }
|
||||||
p2s_address_pool = var.p2s_address_pool
|
wireguard = { enabled = var.wireguard_enabled, address = var.wireguard_address, listen_port = var.wireguard_listen_port }
|
||||||
p2s_ca_name = "VPN Router CA"
|
}
|
||||||
wg_enabled = var.wireguard_enabled
|
|
||||||
wg_address = var.wireguard_address
|
|
||||||
wg_listen_port = var.wireguard_listen_port
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cloud_init_rendered = local.supply_pki ? templatefile("${path.module}/../cloud-init-with-pki.yaml.tpl", merge(local.cloud_init_vars, {
|
cloud_init_rendered = local.supply_pki ? templatefile("${path.module}/../cloud-init-with-pki.yaml.tpl", merge(local.cloud_init_vars, {
|
||||||
|
|||||||
+43
-37
@@ -10,18 +10,19 @@ set -e
|
|||||||
|
|
||||||
DEB="${DEB:-./vpn-router_1.0.0-1_all.deb}"
|
DEB="${DEB:-./vpn-router_1.0.0-1_all.deb}"
|
||||||
|
|
||||||
|
# List values are JSON arrays.
|
||||||
PLATFORM="${PLATFORM:-generic}"
|
PLATFORM="${PLATFORM:-generic}"
|
||||||
MODE="${MODE:-manual}"
|
MODE="${MODE:-manual}"
|
||||||
EXTERNAL_INTERFACE="${EXTERNAL_INTERFACE:-eth0}"
|
EXTERNAL_INTERFACE="${EXTERNAL_INTERFACE:-eth0}"
|
||||||
INTERNAL_INTERFACE="${INTERNAL_INTERFACE:-eth1}"
|
INTERNAL_INTERFACE="${INTERNAL_INTERFACE:-eth1}"
|
||||||
LOCAL_FQDN="${LOCAL_FQDN:-router.example.com}"
|
LOCAL_FQDN="${LOCAL_FQDN:-router.example.com}"
|
||||||
LOCAL_ID_MODE="${LOCAL_ID_MODE:-fqdn}"
|
LOCAL_ID_MODE="${LOCAL_ID_MODE:-fqdn}"
|
||||||
LOCAL_CIDRS="${LOCAL_CIDRS:-10.0.0.0/24}"
|
LOCAL_CIDRS="${LOCAL_CIDRS:-[\"10.0.0.0/24\"]}"
|
||||||
INT_ADDR="${INT_ADDR:-10.1.1.4}"
|
INT_ADDR="${INT_ADDR:-10.1.1.4}"
|
||||||
INT_GATEWAY_IP="${INT_GATEWAY_IP:-10.1.1.1}"
|
INT_GATEWAY_IP="${INT_GATEWAY_IP:-10.1.1.1}"
|
||||||
REMOTE_ADDRS="${REMOTE_ADDRS:-peer.example.net}"
|
REMOTE_ADDRS="${REMOTE_ADDRS:-[\"peer.example.net\"]}"
|
||||||
REMOTE_ID="${REMOTE_ID:-peer.example.net}"
|
REMOTE_ID="${REMOTE_ID:-peer.example.net}"
|
||||||
REMOTE_CIDRS="${REMOTE_CIDRS:-192.168.0.0/24}"
|
REMOTE_CIDRS="${REMOTE_CIDRS:-[\"192.168.0.0/24\"]}"
|
||||||
PSK="${PSK:-change-me}"
|
PSK="${PSK:-change-me}"
|
||||||
P2S_ENABLED="${P2S_ENABLED:-false}"
|
P2S_ENABLED="${P2S_ENABLED:-false}"
|
||||||
P2S_ADDRESS_POOL="${P2S_ADDRESS_POOL:-172.16.0.0/24}"
|
P2S_ADDRESS_POOL="${P2S_ADDRESS_POOL:-172.16.0.0/24}"
|
||||||
@@ -30,42 +31,47 @@ WG_ENABLED="${WG_ENABLED:-false}"
|
|||||||
WG_ADDRESS="${WG_ADDRESS:-}"
|
WG_ADDRESS="${WG_ADDRESS:-}"
|
||||||
WG_LISTEN_PORT="${WG_LISTEN_PORT:-51820}"
|
WG_LISTEN_PORT="${WG_LISTEN_PORT:-51820}"
|
||||||
|
|
||||||
# Write the configuration before installing, so postinst leaves it alone.
|
# Write the configuration before installing, so postinst leaves it alone. The
|
||||||
|
# pre-shared key goes to its own file, so it needs no JSON escaping.
|
||||||
install -d -m 0755 /etc/vpn-router
|
install -d -m 0755 /etc/vpn-router
|
||||||
umask 077
|
umask 077
|
||||||
cat > /etc/vpn-router/vpn-router.conf <<EOF
|
printf %s "$PSK" > /etc/vpn-router/psk
|
||||||
[general]
|
cat > /etc/vpn-router/vpn-router.json <<EOF
|
||||||
platform = ${PLATFORM}
|
{
|
||||||
mode = ${MODE}
|
"general": {
|
||||||
|
"platform": "${PLATFORM}",
|
||||||
[interfaces]
|
"mode": "${MODE}"
|
||||||
external = ${EXTERNAL_INTERFACE}
|
},
|
||||||
internal = ${INTERNAL_INTERFACE}
|
"interfaces": {
|
||||||
|
"external": "${EXTERNAL_INTERFACE}",
|
||||||
[wan]
|
"internal": "${INTERNAL_INTERFACE}"
|
||||||
local_fqdn = ${LOCAL_FQDN}
|
},
|
||||||
local_id_mode = ${LOCAL_ID_MODE}
|
"wan": {
|
||||||
|
"local_fqdn": "${LOCAL_FQDN}",
|
||||||
[local]
|
"local_id_mode": "${LOCAL_ID_MODE}"
|
||||||
cidrs = ${LOCAL_CIDRS}
|
},
|
||||||
int_addr = ${INT_ADDR}
|
"local": {
|
||||||
int_gateway_ip = ${INT_GATEWAY_IP}
|
"cidrs": ${LOCAL_CIDRS},
|
||||||
|
"int_addr": "${INT_ADDR}",
|
||||||
[remote]
|
"int_gateway_ip": "${INT_GATEWAY_IP}"
|
||||||
addrs = ${REMOTE_ADDRS}
|
},
|
||||||
id = ${REMOTE_ID}
|
"remote": {
|
||||||
cidrs = ${REMOTE_CIDRS}
|
"addrs": ${REMOTE_ADDRS},
|
||||||
psk_b64 = $(printf %s "$PSK" | base64 -w0)
|
"id": "${REMOTE_ID}",
|
||||||
|
"cidrs": ${REMOTE_CIDRS},
|
||||||
[p2s]
|
"psk_file": "/etc/vpn-router/psk"
|
||||||
enabled = ${P2S_ENABLED}
|
},
|
||||||
address_pool = ${P2S_ADDRESS_POOL}
|
"p2s": {
|
||||||
ca_name = ${P2S_CA_NAME}
|
"enabled": ${P2S_ENABLED},
|
||||||
|
"address_pool": "${P2S_ADDRESS_POOL}",
|
||||||
[wireguard]
|
"ca_name": "${P2S_CA_NAME}"
|
||||||
enabled = ${WG_ENABLED}
|
},
|
||||||
address = ${WG_ADDRESS}
|
"wireguard": {
|
||||||
listen_port = ${WG_LISTEN_PORT}
|
"enabled": ${WG_ENABLED},
|
||||||
|
"address": "${WG_ADDRESS}",
|
||||||
|
"listen_port": ${WG_LISTEN_PORT}
|
||||||
|
}
|
||||||
|
}
|
||||||
EOF
|
EOF
|
||||||
umask 022
|
umask 022
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user