Мониторинг статуса 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 секунд. Он делает четыре вещи:

  1. WireGuard dump — запускает wg show wg0 dump и парсит вывод, разделённый табуляцией. Для каждого пира извлекает: публичный ключ (первые 8 символов), туннельный IP, время последнего handshake, байты RX/TX и признак «онлайн» (handshake за последние 3 минуты).

  2. История эндпоинтов WireGuard — ведёт JSON-файл /var/lib/vpn-status/wg-history.json. Каждый раз, когда у пира эндпоинт отличается от (none), в историю добавляются метка времени и IP:port. История обрезается до 24 часов. Это даёт примерный лог подключений без внешней инфраструктуры логирования.

  3. IKEv2-сессии — запускает swanctl --list-sas --raw и парсит вывод на предмет активных child SA.

  4. Статистика прокси — собирает счётчики запросов из 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