Gateway over AmneziaWG
Шлюз (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
Запуск
- Создай папку /volume1/docker/gateway и положи туда compose.yaml, Dockerfile, entrypoint.sh, awg0.conf.
- Создай подпапку darkstat (Container Manager сам её не создаёт) и, по желанию, файл hosts-names внутри.
- Проверь родительский интерфейс macvlan: обычно ovs_eth0 (с Open vSwitch) или eth0. Подставь свою подсеть и адрес роутера в секции networks.
- Собери и подними проект: 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 смонтирован томом, поэтому его правки применяются простым перезапуском контейнера, без пересборки.