Создаем сеть:
docker network create vpn
Готовим образ с Wireguard:
FROM debian:trixie-slim@sha256:77ba0164de17b88dd0bf6cdc8f65569e6e5fa6cd256562998b62553134a00ef0
# wg-quick requirements: iproute2, procps
# envsubst util: gettext-base
RUN apt-get update && \
apt-get install -y wireguard iproute2 procps gettext-base wget && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Patch wg-quick if getting "sysctl: permission denied on key net.ipv4.conf.all.src_valid_mark" error.
#
# Credits: @kianbahasadri from (https://forums.docker.com/t/sysctl-error-setting-key-net-ipv4-conf-all-src-valid-mark-read-only-file-system/92567/10)
# and https://github.com/linuxserver/docker-wireguard
#
# See also
# https://github.com/wg-easy/wg-easy/issues/2261
# and
# https://github.com/WireGuard/wireguard-tools/pull/29/files
RUN sed -i 's|\[\[ $proto == -4 \]\] && cmd sysctl -q net\.ipv4\.conf\.all\.src_valid_mark=1|[[ $proto == -4 ]] \&\& [[ $(sysctl -n net.ipv4.conf.all.src_valid_mark) != 1 ]] \&\& cmd sysctl -q net.ipv4.conf.all.src_valid_mark=1|' /usr/bin/wg-quick
RUN mkdir /opt/app
WORKDIR /opt/app
RUN wget -O - https://github.com/DNSCrypt/dnscrypt-proxy/releases/download/2.1.15/dnscrypt-proxy-linux_x86_64-2.1.15.tar.gz | tar -xz && cp linux-x86_64/dnscrypt-proxy ./ && rm -r linux-x86_64
COPY ./dnscrypt-proxy.toml ./
COPY ./wg0.conf-template ./
Можно взять linuxserver/wireguard и не накладывать патч на wg-quick из-за ограничений в контейнере
name: vpn
services:
wireguard:
image: wireguard
build: .
container_name: vpn
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
sysctls:
# Patch wg-quick if getting "sysctl: permission denied on key net.ipv4.conf.all.src_valid_mark" error.
#
# Credits: @kianbahasadri from (https://forums.docker.com/t/sysctl-error-setting-key-net-ipv4-conf-all-src-valid-mark-read-only-file-system/92567/10)
# and https://github.com/linuxserver/docker-wireguard
#
# See also
# https://github.com/wg-easy/wg-easy/issues/2261
# and
# https://github.com/WireGuard/wireguard-tools/pull/29/files
- net.ipv4.conf.all.src_valid_mark=1
networks:
- vpn
dns:
- 127.0.0.1
command: bash -c "envsubst < wg0.conf-template > /etc/wireguard/wg0.conf && wg-quick up wg0 && ./dnscrypt-proxy"
environment:
# required
- WG_ADDRESS=
- WG_PRIVATEKEY=
- WG_PEER_PUBLICKEY=
- WG_PEER_ENDPOINT=
# Network creation: `docker network create vpn`
networks:
vpn:
external: trueЗадаем нужные значения для WG_* переменных в docker-compose.yml
wg0.conf-template:
[Interface]
Address = ${WG_ADDRESS}
ListenPort = 51820
PrivateKey = ${WG_PRIVATEKEY}
[Peer]
PublicKey = ${WG_PEER_PUBLICKEY}
Endpoint = ${WG_PEER_ENDPOINT}
AllowedIPs = 0.0.0.0/0Значения переменных заполняются через envsubst.
Собираем и запускаем:
docker compose build --progress plain wireguard
docker compose up -d wireguard
Допустим, есть приложение в контейнере (app), с отдельным Docker Compose файлом, которое нужно пустить в сеть через Wireguard и оно зависит еще от одного приложения (redis):
services:
app:
image: debian:trixie-slim@sha256:77ba0164de17b88dd0bf6cdc8f65569e6e5fa6cd256562998b62553134a00ef0
network_mode: "container:vpn"
depends_on:
- redis
redis:
image: redis:8.4.0-bookworm@sha256:73dad4271642c5966db88db7a7585fae7cf10b685d1e48006f31e0294c29fdd7
networks:
- vpn
networks:
vpn:
external: trueЗапускаем: docker compose up -d app
Проверяем, что приложение ходит по сети через тоннель: docker compose run --rm app bash -c "apt-get update && apt-get install -y curl && curl 'https://ident.me'"
Еще хорошо бы ��роверить (tcpdump + Wireshark), что Wireguard контейнер не шлет DNS запросы в обход Wireguard тоннеля.
Проверяем, что зависимость приложения (redis) доступна по имени сервиса: docker compose run --rm app bash -c "apt-get update && apt-get install -y redis-tools && redis-cli -h redis set a b"
Как это работает
network_mode: "container:vpn" помещает контейнер app в сеть контейнера wireguard. По документации не очень понятно, что кроме ID контейнера (который CONTAINER ID из docker ps) можно указывать и имя контейнера (NAMES из docker ps). Интересно, что docker inspect app-app-1 не показывает, что контейнер вообще состоит в какой-нибудь сети (пустые поля в "NetworkSettings").
container_name: vpn задает удобное короткое имя контейнера, чтобы не указывать wireguard-wireguard-1
Без добавления redis во внешнюю сеть vpn, в которой состоит и wireguard, app бы не смог найти redis по имени, ведь он видит по сети только то, что видит wireguard.
DNS запросы из app идут через wireguard, но без дополнительных настроек они бы шли далее через машину, где запущен контейнер wireguard. Один из способов это исправить - слать "DNS over HTTPS" запросы через тоннель. Для этого контейнер wireguard настроен (dns: - 127.0.0.1) на локальный DNSCrypt.
dnscrypt-proxy.toml:
server_names = ['static_cloudflare']
listen_addresses = ['127.0.0.1:53']
log_level = 5
use_syslog = true
ignore_system_dns = true
cache = true
[static]
[static.static_cloudflare]
# See https://github.com/DNSCrypt/dnscrypt-resolvers/blob/master/v3/public-resolvers.md
# and
# https://adguard-dns.io/kb/miscellaneous/create-dns-stamp/
stamp = 'sdns://AgcAAAAAAAAABzEuMC4wLjEAEmRucy5jbG91ZGZsYXJlLmNvbQovZG5zLXF1ZXJ5'DNSCrypt по-умолчанию берет DNS сервера из внешнего списка, который он запрашивает, используя системный DNS. Эти настройки указывают брать заданные адреса серверов (server_names) и не использовать системный DNS (ignore_system_dns). Для примера взят сервер CloudFlare в формате DNS stamp
У этого подхода есть большой минус: Контейнер app потеряет сеть, если wireguard контейнер перезагрузится. Способ вернуть app доступ в сеть через wireguard без перезагрузки app автору неизвестен.
P.S.
Слишком коротко для статьи, слишком длинно для поста.
Дополнение: Добавляем Wireguard к Docker образу без изменений в Dockerfile и docker-compose.yml.
Комментарии к статье (спасибо andy2000) подтолкнули к поиску другого решения: добавить Wireguard к образу, не меняя конфигурацию, и запускать контейнер из оригинального или дополненного образа. Чтобы дополнить Dockerfile можно было бы использовать INCLUDE+, но это нестандартное расширение команд Dockerfile. Philip Couling на StackOverflow предложил использовать buildx bake.
docker-compose.yml:
services:
app:
image: local-image:latest
build:
context: .
command: sleep 3600
depends_on:
- redis
redis:
image: redis:8.4.0-bookworm@sha256:73dad4271642c5966db88db7a7585fae7cf10b685d1e48006f31e0294c29fdd7
docker-compose.vpn.yml:
services:
app:
image: local-image:vpn
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
sysctls:
# Patch wg-quick if getting "sysctl: permission denied on key net.ipv4.conf.all.src_valid_mark" error.
#
# Credits: @kianbahasadri from (https://forums.docker.com/t/sysctl-error-setting-key-net-ipv4-conf-all-src-valid-mark-read-only-file-system/92567/10)
# and https://github.com/linuxserver/docker-wireguard
#
# See also
# https://github.com/wg-easy/wg-easy/issues/2261
# and
# https://github.com/WireGuard/wireguard-tools/pull/29/files
- net.ipv4.conf.all.src_valid_mark=1
dns:
- 127.0.0.1
environment:
- WG_ADDRESS= # required. e.g. 10.0.0.2/24
- WG_PRIVATEKEY= # required
- WG_PEER_PUBLICKEY= # required
- WG_PEER_ENDPOINT= # required. e.g. 1.1.1.1:51821
Тут те же настройки для запуска Wireguard, как в предыдущем подходе, только другое имя образа - local-image:vpn.
Dockerfile.vpn:
# see docker-bake.hcl
FROM main-dockerfile
# wg-quick requirements: iproute2, procps
# envsubst util: gettext-base
RUN apt-get update && \
apt-get install -y wireguard iproute2 procps gettext-base wget wait-for-it && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Patch wg-quick if getting "sysctl: permission denied on key net.ipv4.conf.all.src_valid_mark" error.
#
# Credits: @kianbahasadri from (https://forums.docker.com/t/sysctl-error-setting-key-net-ipv4-conf-all-src-valid-mark-read-only-file-system/92567/10)
# and https://github.com/linuxserver/docker-wireguard
#
# See also
# https://github.com/wg-easy/wg-easy/issues/2261
# and
# https://github.com/WireGuard/wireguard-tools/pull/29/files
RUN sed -i 's|\[\[ $proto == -4 \]\] && cmd sysctl -q net\.ipv4\.conf\.all\.src_valid_mark=1|[[ $proto == -4 ]] \&\& [[ $(sysctl -n net.ipv4.conf.all.src_valid_mark) != 1 ]] \&\& cmd sysctl -q net.ipv4.conf.all.src_valid_mark=1|' /usr/bin/wg-quick
RUN mkdir /opt/app
WORKDIR /opt/app
RUN wget -O - https://github.com/DNSCrypt/dnscrypt-proxy/releases/download/2.1.15/dnscrypt-proxy-linux_x86_64-2.1.15.tar.gz | tar -xz && cp linux-x86_64/dnscrypt-proxy ./ && rm -r linux-x86_64
COPY ./dnscrypt-proxy.toml ./wg0.conf-template ./run.sh ./
RUN ./dnscrypt-proxy -service install
ENTRYPOINT ["./run.sh"]
run.sh
#!/usr/bin/env bash
set -e
envsubst < wg0.conf-template > /etc/wireguard/wg0.conf
wg-quick up wg0
./dnscrypt-proxy -service start
wait-for-it 127.0.0.1:53
exec "$@"Тут сборка Wireguard, как в подходе выше, но ссылка на этап сборки main-dockerfile, который будет объявлен в docker-bake.hcl
docker-bake.hcl:
target "app-vpn" {
tags = ["local-image:vpn"]
dockerfile = "Dockerfile.vpn"
contexts = {
// Credits: Philip Couling
// https://stackoverflow.com/questions/36362233/can-a-dockerfile-extend-another-one
//
// Target `app` will be automatically parsed from docker-compose.yml
main-dockerfile = "target:app"
}
}Он позволяет ссылаться в Dockerfile.vpn на этап сборки main-dockerfile как на собранный образ app, даже если он еще не собран и без тега. Docker Bake посмотрит в docker-compose.yml, найдет там сервис app и запустит его сборку, если ее еще не было.
Обычная сборка и запуск приложения без Wireguard не изменятся: docker compose up app
Сборка приложения с Wireguard: docker buildx bake app-vpn
Запуск приложения с Wireguard: docker compose -f docker-compose.yml -f docker-compose.vpn.yml -f docker-compose.override.yml up app
Предполагается, что еще есть docker-compose.override.yml, где переопределены WG_* переменные.
