Шлюз (Gateway) через AmneziaWG на Synology

Контейнер получает собственный IP в локальной сети (через macvlan) и работает как VPN-шлюз: любое устройство, у которого этот IP указан шлюзом по умолчанию, выходит в интернет через AmneziaWG. Плюс встроен watchdog (авто-переподключение туннеля) и веб-панель darkstat со статистикой трафика по каждому IP.

Проект — папка /volume1/docker/gateway с файлами: compose.yaml, Dockerfile, entrypoint.sh, awg0.conf (отдельный экспорт из Amnezia, не тот же, что у socks) и подпапка darkstat с файлом hosts-names.

compose.yaml

Тут задаётся статический адрес шлюза 192.168.11.253 и родительский интерфейс macvlan ovs_eth0 (на Synology с Open vSwitch — именно он; если vSwitch выключен, будет eth0). Подсеть и адрес роутера подставь свои.

services: gateway: build: . image: gateway:latest container_name: gateway restart: unless-stopped # privileged: sysctl (ip_forward) + tun + iptables privileged: true devices: - /dev/net/tun:/dev/net/tun environment: CHECK_INTERVAL: "30" FAIL_THRESHOLD: "2" MON_PORT: "80" networks: lan: ipv4_address: 192.168.11.253 volumes: - ./awg0.conf:/etc/amnezia/amneziawg/awg0.conf:ro - ./entrypoint.sh:/entrypoint.sh:ro - ./darkstat:/var/lib/darkstat healthcheck: test: ["CMD-SHELL", "wget -qO- -T 8 https://1.1.1.1/cdn-cgi/trace | grep -q '^ip=' || exit 1"] interval: 30s timeout: 12s retries: 3 start_period: 25s networks: lan: driver: macvlan driver_opts: parent: ovs_eth0 ipam: config: - subnet: 192.168.11.0/24 gateway: 192.168.11.254

Dockerfile

Отличие от socks — ставится iptables-legacy (ядро Synology 4.4 не умеет nftables) для NAT/MASQUERADE и darkstat для мониторинга.

# ---- amneziawg-go (userspace WireGuard с обфускацией) ---- FROM golang:alpine AS gobuild RUN apk add --no-cache git make gcc musl-dev linux-headers RUN git clone --depth=1 https://github.com/amnezia-vpn/amneziawg-go /src/awg-go WORKDIR /src/awg-go RUN go build -o /out/amneziawg-go . # ---- amneziawg-tools (awg, awg-quick) ---- FROM alpine:3.20 AS toolsbuild RUN apk add --no-cache git make gcc musl-dev linux-headers bash RUN git clone --depth=1 https://github.com/amnezia-vpn/amneziawg-tools /src/awg-tools WORKDIR /src/awg-tools/src RUN make && make install # ---- финальный образ (iptables нужен для NAT/MASQUERADE шлюза) ---- FROM alpine:3.20 RUN apk add --no-cache bash iproute2 iptables iptables-legacy ca-certificates openresolv darkstat COPY --from=gobuild /out/amneziawg-go /usr/bin/amneziawg-go COPY --from=toolsbuild /usr/bin/awg /usr/bin/awg COPY --from=toolsbuild /usr/bin/awg-quick /usr/bin/awg-quick COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"]

entrypoint.sh

#!/bin/bash set -e mkdir -p /dev/net if [ ! -c /dev/net/tun ]; then mknod /dev/net/tun c 10 200 || true fi export WG_QUICK_USERSPACE_IMPLEMENTATION=amneziawg-go CONF_SRC=/etc/amnezia/amneziawg/awg0.conf CONF=/run/awg/awg0.conf mkdir -p /run/awg CHECK_INTERVAL="${CHECK_INTERVAL:-30}" FAIL_THRESHOLD="${FAIL_THRESHOLD:-2}" HEARTBEAT_EVERY="${HEARTBEAT_EVERY:-10}" PROBE_URL="${PROBE_URL:-https://1.1.1.1/cdn-cgi/trace}" PROBE_TIMEOUT="${PROBE_TIMEOUT:-8}" LAN_SUBNET="${LAN_SUBNET:-192.168.11.0/24}" MON_PORT="${MON_PORT:-667}" MON_FILTER="${MON_FILTER:-not (ether multicast or ether broadcast)}" log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; } if [ ! -s "$CONF_SRC" ]; then log "ОШИБКА: нет конфига $CONF_SRC — положи awg0.conf в папку gateway" exit 1 fi grep -vE '^[[:space:]]*(Table|MTU)[[:space:]]*=' "$CONF_SRC" \ | grep -vE '^[[:space:]]*(I[1-5]|J[1-3]|Itime)[[:space:]]*=[[:space:]]*$' \ | awk '{print} /^\[Interface\]/ && !x {print "Table = off"; print "MTU = 1360"; x=1}' > "$CONF" EP_HOST=$(awk -F'[:= ]+' '/^Endpoint/{print $2}' "$CONF") ORIG_GW=$(ip route show default | awk '{print $3; exit}') ORIG_DEV=$(ip route show default | awk '{print $5; exit}') log "старт: LAN gw=$ORIG_GW dev=$ORIG_DEV endpoint=$EP_HOST" tunnel_up() { awg-quick up "$CONF" EP_IP=$(getent ahostsv4 "$EP_HOST" | awk '{print $1; exit}') if [ -n "$EP_IP" ] && [ -n "$ORIG_GW" ]; then ip route replace "$EP_IP/32" via "$ORIG_GW" dev "$ORIG_DEV" fi ip route replace default dev awg0 log "туннель поднят: endpoint $EP_HOST -> $EP_IP (via $ORIG_GW dev $ORIG_DEV), default->awg0, $(ip link show awg0 | grep -o 'mtu [0-9]*')" } tunnel_down() { awg-quick down "$CONF" >/dev/null 2>&1 || true; } setup_forwarding() { sysctl -w net.ipv4.ip_forward=1 >/dev/null 2>&1 || echo 1 > /proc/sys/net/ipv4/ip_forward 2>/dev/null || true iptables-legacy -t nat -C POSTROUTING -o awg0 -j MASQUERADE 2>/dev/null || iptables-legacy -t nat -A POSTROUTING -o awg0 -j MASQUERADE 2>/dev/null || log "WARN: MASQUERADE не добавился" iptables-legacy -C FORWARD -o awg0 -j ACCEPT 2>/dev/null || iptables-legacy -A FORWARD -o awg0 -j ACCEPT 2>/dev/null || true iptables-legacy -C FORWARD -i awg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || iptables-legacy -A FORWARD -i awg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true iptables-legacy -t mangle -C FORWARD -p tcp --tcp-flags SYN,RST SYN -o awg0 -j TCPMSS --clamp-mss-to-pmtu 2>/dev/null || iptables-legacy -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -o awg0 -j TCPMSS --clamp-mss-to-pmtu 2>/dev/null || log "WARN: MSS-clamp не добавился (модуль xt_TCPMSS?)" log "форвардинг: ip_forward=$(cat /proc/sys/net/ipv4/ip_forward), NAT(MASQUERADE)+MSS-clamp на awg0" } start_monitor() { command -v darkstat >/dev/null 2>&1 || { log "WARN: darkstat не установлен в образе"; return; } mkdir -p /var/lib/darkstat && chmod 0777 /var/lib/darkstat NAMES=/var/lib/darkstat/hosts-names if [ -f "$NAMES" ] && grep -qvE '^[[:space:]]*(#|$)' "$NAMES"; then grep -vE '^[[:space:]]*(#|$)' "$NAMES" >> /etc/hosts 2>/dev/null && log "имена устройств подмешаны из hosts-names" fi set -- -i eth0 -f "$MON_FILTER" -p "$MON_PORT" -b 0.0.0.0 -l 192.168.11.0/255.255.255.0 --local-only --export /var/lib/darkstat/darkstat.db --daylog /var/lib/darkstat/daylog.txt [ -f /var/lib/darkstat/darkstat.db ] && set -- "$@" --import /var/lib/darkstat/darkstat.db if darkstat "$@" 2>/dev/null; then log "darkstat запущен: веб-панель http://192.168.11.253:${MON_PORT} (трафик по IP)" else log "WARN: darkstat не стартовал" fi } reconnect() { log "RECONNECT: связь потеряна, пересоздаю туннель..." tunnel_down; sleep 2 tunnel_up || log "RECONNECT: awg-quick up ошибка, повтор на след. цикле" setup_forwarding log "RECONNECT: завершено" } probe() { local out ip out=$(wget -qO- -T "$PROBE_TIMEOUT" "$PROBE_URL" 2>/dev/null) || return 1 ip=$(printf '%s\n' "$out" | awk -F= '/^ip=/{print $2}') [ -n "$ip" ] || return 1 LAST_EXIT_IP="$ip"; return 0 } tunnel_up setup_forwarding start_monitor awg show || true if probe; then log "проверка связи: OK, выход через $LAST_EXIT_IP"; else log "проверка связи: не прошла сразу после старта (watchdog разберётся)"; fi set +e log "watchdog активен: интервал ${CHECK_INTERVAL}s, порог реконнекта ${FAIL_THRESHOLD}, heartbeat каждые ${HEARTBEAT_EVERY} проверок" FAILS=0; OKN=0 while true; do sleep "$CHECK_INTERVAL" if probe; then if [ "$FAILS" -gt 0 ]; then log "связь ВОССТАНОВЛЕНА, выход через $LAST_EXIT_IP"; fi FAILS=0; OKN=$((OKN+1)) if [ $((OKN % HEARTBEAT_EVERY)) -eq 0 ]; then log "OK: линк жив, выход через $LAST_EXIT_IP"; fi else FAILS=$((FAILS+1)) HS=$(awg show awg0 latest-handshakes 2>/dev/null | awk '{print $2; exit}') NOW=$(date +%s); AGE="n/a"; if [ -n "$HS" ]; then AGE=$((NOW-HS)); fi log "FAIL #${FAILS}/${FAIL_THRESHOLD}: проба не прошла (последний хендшейк ${AGE}s назад)" if [ "$FAILS" -ge "$FAIL_THRESHOLD" ]; then reconnect; FAILS=0; fi fi done

awg0.conf

Отдельный экспорт из Amnezia (AmneziaWG). У шлюза должен быть свой конфиг с собственным адресом и ключами — не переиспользуй тот, что у socks. Параметры Table и MTU проставит entrypoint.sh.

[Interface] Address = 10.8.1.5/32 DNS = 1.1.1.1, 1.0.0.1 PrivateKey = *ваш приватный ключ* Jc = 5 Jmin = 10 Jmax = 50 S1 = 32 S2 = 139 S3 = 52 S4 = 1 H1 = 322017579-1492438178 H2 = 2127325942-2133312985 H3 = 2140135011-2142808705 H4 = 2146777525-2147460943 I1 = <r 2><b 0x858000010001000000000669636c6f756403636f6d0000010001c00c000100010000105a00044d583737> I2 = I3 = I4 = I5 = [Peer] PublicKey = *публичный ключ сервера* PresharedKey = *ваш preshared ключ* AllowedIPs = 0.0.0.0/0, ::/0 Endpoint = vpn.example.com:48105 PersistentKeepalive = 25

darkstat/hosts-names

Справочник, чтобы в веб-панели вместо голых IP показывались имена устройств. Файл лежит в /volume1/docker/gateway/darkstat/hosts-names. После правки — перезапусти контейнер.

# Формат: IP имя (через пробел/таб). Строки с # и пустые игнорируются. # После правки — перезапусти контейнер gateway. #192.168.11.70 telefon #192.168.11.68 noutbuk #192.168.11.250 tv-gostinaya

Запуск

  1. Создай папку /volume1/docker/gateway и положи туда compose.yaml, Dockerfile, entrypoint.sh, awg0.conf.
  2. Создай подпапку darkstat (Container Manager сам её не создаёт) и, по желанию, файл hosts-names внутри.
  3. Проверь родительский интерфейс macvlan: обычно ovs_eth0 (с Open vSwitch) или eth0. Подставь свою подсеть и адрес роутера в секции networks.
  4. Собери и подними проект: docker compose up -d --build (или через Container Manager).

Как пустить устройство через VPN: в сетевых настройках устройства укажи шлюзом по умолчанию 192.168.11.253 (адрес контейнера). Весь его трафик пойдёт через AmneziaWG. Остальные устройства сети работают напрямую, пока им не сменишь шлюз.

Статистика трафика: веб-панель darkstat доступна по адресу http://192.168.11.253 — трафик по каждому IP, с именами из hosts-names. Мультикаст/бродкаст отфильтрованы.

Важно: после правки Dockerfile образ нужно пересобрать (кнопка «Создать» берёт старый кэш) — удали образ gateway:latest и подними проект заново. А вот entrypoint.sh смонтирован томом, поэтому его правки применяются простым перезапуском контейнера, без пересборки.