Мониторинг статуса VPN: WireGuard + IKEv2 в реальном времени
Published: 2026-06-02
Страница VPN на этом сайте показывает данные о пирах в реальном времени — какие WireGuard-клиенты онлайн, когда они последний раз делали handshake, сколько трафика прогнали и какие IKEv2-сессии сейчас держит strongSwan. Вот как это работает.
Два VPN-протокола
На сервере работают два независимых VPN-стека:
- WireGuard — современный, быстрый, на уровне ядра. Клиенты подключаются по UDP 51820. У каждого пира статический IP внутри туннельной сети
10.0.0.0/24. - IKEv2 / IPsec (strongSwan) — полезен для iOS, macOS и Windows, которые поддерживают его нативно без дополнительных приложений. Клиенты получают адрес из пула strongSwan.
Оба настраиваются скриптами из k0s-setup (04-setup-vpn.sh, 08-setup-wireguard.sh) прямо на хосте, не внутри кластера.
Сборщик статуса
Python-скрипт живёт в /usr/local/bin/vpn-status-update на хосте. systemd-таймер запускает его каждые 30 секунд. Он делает четыре вещи:
-
WireGuard dump — запускает
wg show wg0 dumpи парсит вывод, разделённый табуляцией. Для каждого пира извлекает: публичный ключ (первые 8 символов), туннельный IP, время последнего handshake, байты RX/TX и признак «онлайн» (handshake за последние 3 минуты). -
История эндпоинтов WireGuard — ведёт JSON-файл
/var/lib/vpn-status/wg-history.json. Каждый раз, когда у пира эндпоинт отличается от(none), в историю добавляются метка времени и IP:port. История обрезается до 24 часов. Это даёт примерный лог подключений без внешней инфраструктуры логирования. -
IKEv2-сессии — запускает
swanctl --list-sas --rawи парсит вывод на предмет активных child SA. -
Статистика прокси — собирает счётчики запросов из tinyproxy, SOCKS5 и Shadowsocks. Все четыре набора данных объединяются в один JSON-объект и атомарно записываются в
/var/lib/vpn-status/status.json.
FastAPI-приложение читает этот файл при каждом запросе на /vpn/status — никакой базы данных, никаких сокетов, просто чтение файла.
Раздача конфиг-файлов и QR-кодов
Директория vpn/ в репозитории содержит клиентские конфигурационные файлы:
| Файл | Протокол | MIME type |
|---|---|---|
wireguard.conf |
WireGuard | application/octet-stream |
apple.mobileconfig |
IKEv2 для iOS/macOS | application/x-apple-aspen-config |
android.sswan |
IKEv2 для Android | application/vnd.strongswan.profile |
В роутере FastAPI есть явный список разрешённых файлов (ALLOWED_VPN_FILES). Любое имя файла вне этого словаря получает 404. ipsec.env и другие серверные секреты наружу не отдаются.
Для каждого разрешённого файла есть эндпоинт /vpn/qr/{filename}. Он кодирует текстовое содержимое файла в QR-код в формате PNG и кэширует его в памяти:
pythondef _make_qr(filename: str) -> bytes:
if filename not in _qr_cache:
content = (VPN_DIR / filename).read_text()
img = qrcode.make(content)
buf = io.BytesIO()
img.save(buf, format="PNG")
_qr_cache[filename] = buf.getvalue()
return _qr_cache[filename]
Кэш живёт в памяти процесса: переживает запросы, но сбрасывается при перезапуске пода.
Эндпоинт «Мой IP»
/vpn/myip возвращает реальный IP клиента и его геолокацию (город/страну) через ip-api.com. Это делается на стороне сервера, чтобы избежать проблем с CSP при обращении из браузера. Для приватных IP (RFC 1918 и IPv6 ULA) вызов геолокации пропускается. Ответ отдаётся с Cache-Control: no-store, чтобы браузеры его не кэшировали.
Почему не UI для управления VPN?
Tailscale, Headscale, WireGuard Easy — всё это работает, но добавляет сложность и процессы, которые нужно поддерживать. Текущая реализация — три файла и Python-скрипт на 100 строк. Страница статуса только для чтения; пиры я добавляю, редактируя wg0.conf напрямую.
systemd-таймер
ini# /etc/systemd/system/vpn-status-update.service
[Unit]
Description=Обновление JSON статуса VPN
[Service]
Type=oneshot
ExecStart=/usr/local/bin/vpn-status-update
User=root
# /etc/systemd/system/vpn-status-update.timer
[Unit]
Description=Запускать vpn-status-update каждые 30 секунд
[Timer]
OnBootSec=30s
OnUnitActiveSec=30s
[Install]
WantedBy=timers.target
bashsystemctl enable --now vpn-status-update.timer
systemctl list-timers vpn-status-update.timer
Управление пирами WireGuard
Добавление нового пира:
bash# Генерация ключей на сервере
wg genkey | tee /tmp/peer_private | wg pubkey > /tmp/peer_public
# Добавление в wg0.conf
cat >> /etc/wireguard/wg0.conf << EOF
[Peer]
PublicKey = $(cat /tmp/peer_public)
AllowedIPs = 10.0.0.5/32
EOF
# Применить без перезапуска
wg addconf wg0 <(echo "[Peer]"; echo "PublicKey = $(cat /tmp/peer_public)"; echo "AllowedIPs = 10.0.0.5/32")
Структура файла статуса
Файл /var/lib/vpn-status/status.json:
json{
"wireguard": {
"peers": [
{
"public_key_short": "abc12345",
"tunnel_ip": "10.0.0.2",
"last_handshake": 1720000000,
"online": true,
"rx_bytes": 1234567,
"tx_bytes": 987654
}
]
},
"ikev2": {
"sessions": [
{
"id": "1",
"remote_ip": "1.2.3.4",
"user": "vpnuser"
}
]
},
"updated_at": "2026-06-02T14:30:00Z"
}
FastAPI-эндпоинт возвращает этот JSON напрямую (после проверки аутентификации), а JS в браузере рендерит таблицу.
Сравнение подходов
| Подход | Плюсы | Минусы |
|---|---|---|
| Файл + таймер (текущий) | Просто, без зависимостей | Задержка до 30 сек, нет пуша |
| Prometheus + Grafana | Метрики, алерты, история | Сложнее развернуть |
| wg-easy / Headscale UI | Красивый UI, управление пирами | Ещё один сервис для поддержки |
| Netdata | Автообнаружение, хороший UI | Избыточен для одного VPN |