From d4e41ed4ed933af5bc7d9afe0ffc71e9eeaedba4 Mon Sep 17 00:00:00 2001 From: "Simon L." Date: Mon, 24 Aug 2026 15:03:25 +0200 Subject: [PATCH] add wireguard as community container Signed-off-by: Simon L. --- .github/workflows/wg-easy-update.yml | 74 +++++++++++++++++++ community-containers/readme.md | 1 + community-containers/wireguard/readme.md | 22 ++++++ community-containers/wireguard/wireguard.json | 66 +++++++++++++++++ php/containers-schema.json | 9 ++- php/src/Container/Container.php | 2 + php/src/ContainerDefinitionFetcher.php | 6 ++ php/src/Docker/DockerActionManager.php | 40 ++++++++++ 8 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/wg-easy-update.yml create mode 100644 community-containers/wireguard/readme.md create mode 100644 community-containers/wireguard/wireguard.json diff --git a/.github/workflows/wg-easy-update.yml b/.github/workflows/wg-easy-update.yml new file mode 100644 index 00000000..0ffceea9 --- /dev/null +++ b/.github/workflows/wg-easy-update.yml @@ -0,0 +1,74 @@ +name: wg-easy-update + +on: + workflow_dispatch: + schedule: + - cron: '00 12 * * *' + +permissions: + contents: read + +jobs: + run_update: + name: update wg-easy to the latest major release + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Run wg-easy-update + run: | + set -euo pipefail + CONTAINER_FILE="./community-containers/wireguard/wireguard.json" + + # The container pins the rolling major tag, so only major bumps need a PR + pinned="$(jq -r '.aio_services_v1[0].image_tag' "$CONTAINER_FILE")" + if ! [[ "$pinned" =~ ^[0-9]+$ ]]; then + echo "Expected a major tag like \"15\" but found \"$pinned\"!" + exit 1 + fi + + # Filter out pre-releases like v16.0.0-beta.1 + latest="$( + git ls-remote --tags https://github.com/wg-easy/wg-easy \ + | cut -d/ -f3 \ + | grep -v '\^{}' \ + | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \ + | sed -E 's/^v([0-9]+)\..*/\1/' \ + | sort -n \ + | tail -1 + )" + if [ -z "$latest" ]; then + echo "Could not get the latest wg-easy version!" + exit 1 + fi + + if [ "$latest" -le "$pinned" ]; then + echo "wg-easy is up to date at v$pinned" + exit 0 + fi + + sed -i "s|\"image_tag\": \"$pinned\",|\"image_tag\": \"$latest\",|" "$CONTAINER_FILE" + if [ "$(jq -r '.aio_services_v1[0].image_tag' "$CONTAINER_FILE")" != "$latest" ]; then + echo "Failed to update image_tag!" + exit 1 + fi + + - name: Create Pull Request + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v7 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: wg-easy-update automated change + signoff: true + title: wg-easy major update + body: | + Automated wg-easy community container update to a new major release. + + :warning: Major releases may contain breaking changes. Please check the + [release notes](https://github.com/wg-easy/wg-easy/releases) and that the + `INIT_*` [setup variables](https://github.com/wg-easy/wg-easy/blob/master/docs/content/advanced/config/unattended-setup.md) + still work before merging. + labels: dependencies, 3. to review + milestone: next + branch: wg-easy-container-update diff --git a/community-containers/readme.md b/community-containers/readme.md index 20850771..0104c554 100644 --- a/community-containers/readme.md +++ b/community-containers/readme.md @@ -59,6 +59,7 @@ flowchart TD LDAP(["👥 LLDAP\nLight LDAP / Users"]):::community CADDY(["🌐 Caddy\nReverse Proxy + Geoblocking"]):::community NPMPLUS(["🌐 NPMplus\nNginx Proxy Manager"]):::community + WG(["🔑 WireGuard Easy\nVPN server with web UI"]):::community end ``` diff --git a/community-containers/wireguard/readme.md b/community-containers/wireguard/readme.md new file mode 100644 index 00000000..0b3ecbc9 --- /dev/null +++ b/community-containers/wireguard/readme.md @@ -0,0 +1,22 @@ +## WireGuard Easy +This container bundles [wg-easy](https://github.com/wg-easy/wg-easy), a WireGuard VPN server with a web UI for managing client connections, and auto-configures it for you. + +### Notes +- ⚠️ This container opens a VPN entry point into your server's network. Only add it if you understand the implications and keep it updated! +- ⚠️ This container is only intended to be used on a server in a trusted home network. Do **not** add it on a public VPS or any other server whose IP address is directly reachable from the internet! +- ⚠️ The web UI on port `51821/tcp` is served via plain HTTP (`INSECURE=true`) and is published on **all** interfaces. So if you still run this on a server that is reachable from the internet, you need to block port `51821/tcp` with a firewall, as otherwise the admin UI is reachable by everyone and the credentials that you enter there are transmitted unencrypted. +- ⚠️ This container only works on Linux hosts with Docker installed natively. Docker Desktop (Windows, macOS and Linux) is **not** supported since the containers run inside a virtual machine there whose kernel does not provide the WireGuard module to the container. +- ⚠️ This container requires the `wireguard` kernel module on the host and fails to start without it. It ships as a loadable module (`CONFIG_WIREGUARD=m`) on all common distributions with kernel 5.6 or later, so you may need to load it once via `sudo modprobe wireguard` and make that persistent, e.g. via `echo wireguard | sudo tee /etc/modules-load.d/wireguard.conf`. If your kernel is older, you need to install the module on the host first. +- The admin account is created automatically on the first start. The username is `admin` and the password is the one that you can see next to the container in the AIO interface. +- After adding and starting the container, you can visit `http://ip.address.of.this.server:51821` in order to log in and create clients. Each client can be exported as a config file or QR code. +- Port `51820/udp` needs to be forwarded in your router to this server in order to be able to connect from the internet. Port `51821/tcp` (the web UI) must **not** be forwarded! +- The VPN host address that clients will connect to is set to your Nextcloud domain by default. You can change this in the web UI if you want to connect via a different address. +- This container may conflict with the fail2ban community container as both modify iptables rules. Watch out for unexpected behavior if you enable both. +- The configuration and clients of WireGuard will be automatically included in AIOs backup solution! +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/wg-easy/wg-easy + +### Maintainer +https://github.com/szaimen diff --git a/community-containers/wireguard/wireguard.json b/community-containers/wireguard/wireguard.json new file mode 100644 index 00000000..ed8cbe95 --- /dev/null +++ b/community-containers/wireguard/wireguard.json @@ -0,0 +1,66 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-wireguard", + "display_name": "WireGuard Easy", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/wireguard", + "image": "ghcr.io/wg-easy/wg-easy", + "image_tag": "15", + "internal_port": "51821", + "init": false, + "restart": "unless-stopped", + "ports": [ + { + "ip_binding": "", + "port_number": "51820", + "protocol": "udp" + }, + { + "ip_binding": "", + "port_number": "51821", + "protocol": "tcp" + } + ], + "cap_add": [ + "NET_ADMIN", + "SYS_MODULE" + ], + "sysctls": [ + "net.ipv4.ip_forward=1", + "net.ipv4.conf.all.src_valid_mark=1", + "net.ipv6.conf.all.disable_ipv6=0", + "net.ipv6.conf.all.forwarding=1", + "net.ipv6.conf.default.forwarding=1" + ], + "environment": [ + "TZ=%TIMEZONE%", + "INSECURE=true", + "PORT=51821", + "INIT_ENABLED=true", + "INIT_USERNAME=admin", + "INIT_PASSWORD=%WIREGUARD_PASSWORD%", + "INIT_HOST=%NC_DOMAIN%", + "INIT_PORT=51820" + ], + "volumes": [ + { + "source": "nextcloud_aio_wireguard", + "destination": "/etc/wireguard", + "writeable": true + }, + { + "source": "/lib/modules", + "destination": "/lib/modules", + "writeable": false + } + ], + "backup_volumes": [ + "nextcloud_aio_wireguard" + ], + "ui_secret": "WIREGUARD_PASSWORD", + "secrets": [ + "WIREGUARD_PASSWORD" + ] + } + ] +} diff --git a/php/containers-schema.json b/php/containers-schema.json index e7b53708..3f7a891d 100644 --- a/php/containers-schema.json +++ b/php/containers-schema.json @@ -38,6 +38,13 @@ "pattern": "^[A-Z_]+$" } }, + "sysctls": { + "type": "array", + "items": { + "type": "string", + "pattern": "^net\\.(ipv4|ipv6|mptcp|unix)\\.[a-z0-9_.]+=[a-zA-Z0-9_.:/ -]+$" + } + }, "depends_on": { "type": "array", "items": { @@ -220,7 +227,7 @@ }, "source": { "type": "string", - "pattern": "^((nextcloud_aio_[a-z_]+)|(%[A-Z_]+%)|(/dev)|(/run/udev))$" + "pattern": "^((nextcloud_aio_[a-z_]+)|(%[A-Z_]+%)|(/dev)|(/run/udev)|(/lib/modules))$" }, "writeable": { "type": "boolean" diff --git a/php/src/Container/Container.php b/php/src/Container/Container.php index 6b6c5af9..6fb43d9a 100644 --- a/php/src/Container/Container.php +++ b/php/src/Container/Container.php @@ -27,6 +27,8 @@ readonly class Container { public bool $enableNvidiaGpu, /** @var string[] */ public array $capAdd, + /** @var string[] */ + public array $sysctls, public int $shmSize, public bool $apparmorUnconfined, /** @var string[] */ diff --git a/php/src/ContainerDefinitionFetcher.php b/php/src/ContainerDefinitionFetcher.php index 44ab329e..e1c625a0 100644 --- a/php/src/ContainerDefinitionFetcher.php +++ b/php/src/ContainerDefinitionFetcher.php @@ -308,6 +308,11 @@ readonly class ContainerDefinitionFetcher { $capAdd = $entry['cap_add']; } + $sysctls = []; + if (isset($entry['sysctls'])) { + $sysctls = $entry['sysctls']; + } + $shmSize = -1; if (isset($entry['shm_size'])) { $shmSize = $entry['shm_size']; @@ -370,6 +375,7 @@ readonly class ContainerDefinitionFetcher { $devices, $enableNvidiaGpu, $capAdd, + $sysctls, $shmSize, $apparmorUnconfined, $backupVolumes, diff --git a/php/src/Docker/DockerActionManager.php b/php/src/Docker/DockerActionManager.php index a7666d34..e6ada057 100644 --- a/php/src/Docker/DockerActionManager.php +++ b/php/src/Docker/DockerActionManager.php @@ -229,6 +229,24 @@ readonly class DockerActionManager { } } + /** + * Whether the aio network is ipv6 enabled. If it is not, ipv6 sysctls must not + * be applied to containers since setting them fails on hosts whose kernel was + * booted with ipv6.disable=1. + */ + private function IsIpv6EnabledOnNetwork(string $network = 'nextcloud-aio'): bool { + $url = $this->BuildApiUrl(sprintf('networks/%s', urlencode($network))); + try { + $response = $this->sendHttpRequest('GET', $url); + } catch (RequestException $e) { + return false; + } + + $responseBody = json_decode((string)$response->getBody(), true, 512, JSON_THROW_ON_ERROR); + + return ($responseBody['EnableIPv6'] ?? false) === true; + } + public function CreateContainer(Container $container): void { $volumes = []; foreach ($container->volumes->GetVolumes() as $volume) { @@ -401,6 +419,28 @@ readonly class DockerActionManager { $requestBody['HostConfig']['CapAdd'] = $capAdds; } + // Only namespaced sysctls can be set per container. The schema restricts + // these to net.ipv4/net.ipv6/net.mptcp/net.unix which live in the network + // namespace. Non-namespaced ones like vm.overcommit_memory (redis), + // vm.max_map_count (fulltextsearch) and net.core.rmem_max (apache) must + // still be set on the host. See the linked discussions in the docs. + $sysctls = []; + $isIpv6Enabled = count($container->sysctls) > 0 && $this->IsIpv6EnabledOnNetwork(); + foreach ($container->sysctls as $sysctl) { + // Setting net.ipv6.* fails if the host kernel was booted with ipv6.disable=1 + // and has no effect if the aio network itself is not ipv6 enabled. + if (!$isIpv6Enabled && str_starts_with($sysctl, 'net.ipv6.')) { + continue; + } + $sysctlParts = explode('=', $sysctl, 2); + if (count($sysctlParts) === 2) { + $sysctls[$sysctlParts[0]] = $sysctlParts[1]; + } + } + if (count($sysctls) > 0) { + $requestBody['HostConfig']['Sysctls'] = $sysctls; + } + // Disable arp spoofing if (!in_array('NET_RAW', $capAdds, true)) { $requestBody['HostConfig']['CapDrop'] = ['NET_RAW'];