ADG Оставить заявку
Блог Инфраструктура 6 мин чтения

Active-Active VPN с BGP failover: два StrongSwan-шлюза, одна /29 и переключение за 3 секунды

Строим отказоустойчивый VPN на двух StrongSwan-шлюзах с BGP через Bird: оба анонсируют одну /29, healthcheck скриптом, мониторинг туннелей в Zabbix через trap.

Контекст момента

Рост зависимости от удалённого доступа в 2020 поднял требования к отказоустойчивости VPN-шлюзов

С весны количество постоянных VPN-сессий у клиентов не возвращается к прежнему уровню - люди продолжают работать удалённо, и шлюз из «одна из нескольких точек входа» превратился в «если упало - всё стоит». Один из клиентов поставил задачу прямо: у нас сейчас один StrongSwan-шлюз, он падает раз в несколько месяцев из-за апдейтов или проблем с провайдером, и каждый раз это 10-15 минут простоя. Хочется переключение без участия человека и в разумные секунды.

Вариант с VRRP мы рассматривали первым - он очевидный и простой в конфигурации. Проблема в том, что у клиента два шлюза на разных площадках с разными провайдерами: площадки не в одной L2-сети. VRRP без общего broadcast-домена требует туннелей между самими шлюзами, что добавляет слой и делает failover зависимым от качества этого туннеля. Решили идти через BGP.

Схема: два шлюза, одна /29, два аплинка

Архитектура простая. У клиента есть /29-подсеть для VPN-шлюзов, выданная провайдером. Оба шлюза (назовём их gw1 и gw2) подключены к разным BGP-пирам - gw1 к провайдеру A, gw2 к провайдеру B. Каждый анонсирует одну и ту же /29-подсеть в BGP. В нормальном состоянии оба анонса активны, трафик распределяется по маршрутам - получается Active-Active, а не Active-Standby.

При падении одного из шлюзов его BGP-сессия с пиром рвётся, анонс /29 через этот аплинк пропадает, и через несколько секунд весь трафик идёт через живой шлюз. IKE-сессии активных пользователей рвутся и переустанавливаются - это неизбежно, но у StrongSwan с keyingtries=0 и closeaction=restart клиент переподключается автоматически, пользователь видит секундный лаг.

BGP на шлюзах поднимаем через Bird 1.6 - он стабилен, хорошо документирован и не тащит за собой лишних зависимостей. Конфиг на gw1 выглядит примерно так:

router id <ip-gw1>;

protocol bgp uplink_a {
    local as 65001;
    neighbor <ip-provider-a> as 1234;
    import none;
    export filter {
        if net = <vpn-subnet>/29 then accept;
        reject;
    };
}

protocol static vpn_routes {
    route <vpn-subnet>/29 blackhole;
}

route blackhole - это намеренно. Bird не анонсирует маршрут если он не присутствует в таблице маршрутизации. Blackhole-маршрут всегда там есть, пока шлюз жив. Когда шлюз падает - Bird останавливается, BGP-сессия рвётся, анонс уходит. Просто и предсказуемо.

Healthcheck: когда сам процесс жив, а туннели нет

Первая версия схемы работала, но была слепа к одному сценарию: Bird и StrongSwan запущены, процессы живы, BGP-сессия держится - но IKE-туннели не устанавливаются из-за проблем на уровне IPsec. Шлюз с точки зрения BGP «здоров», но реально не пропускает трафик.

Для этого написали healthcheck-скрипт, который запускается каждые 30 секунд (два cron-задания со сдвигом через sleep 30):

#!/bin/bash

THRESHOLD=1  # минимальное число активных туннелей

active=$(ipsec statusall 2>/dev/null | grep -c "ESTABLISHED")

if [ "$active" -lt "$THRESHOLD" ]; then
    logger -t vpn-healthcheck "WARN: active tunnels=$active, threshold=$THRESHOLD - withdrawing BGP"
    birdc disable uplink_a
else
    birdc enable uplink_a
fi

birdc disable снимает пир с анонса не останавливая Bird - шлюз уходит из ротации, пока туннели не восстановятся. birdc enable возвращает его обратно. THRESHOLD выставлен в 1 - если нет ни одного активного туннеля, скрипт считает шлюз нерабочим.

На практике порог подбирается под конкретную инсталляцию: у клиента постоянно присутствуют site-to-site туннели к дочерним офисам, которые должны быть подняты всегда. Их и проверяем. Клиентские road-warrior сессии в расчёт не берём - их число непостоянно.

Мониторинг туннелей в Zabbix через trap

Healthcheck решает автоматику, но нужна ещё видимость: какие туннели активны, когда падали, сколько держатся. Решили через Zabbix SNMP trap - StrongSwan умеет отправлять уведомления через updown-скрипт при установке и разрыве туннеля.

В /etc/strongswan.d/charon.conf включаем:

charon {
    install_routes = no  # маршруты управляем сами
}

В конфигурации соединения добавляем leftupdown=/usr/local/bin/vpn-updown.sh. Скрипт срабатывает при каждом изменении состояния туннеля:

#!/bin/bash
# Вызывается StrongSwan с переменными окружения:
# PLUTO_VERB: up-client, down-client, up-host, down-host
# PLUTO_PEER: IP удалённого пира
# PLUTO_CONNECTION: имя соединения

ZABBIX_SERVER="<zabbix-ip>"
ZABBIX_HOST="vpn-gw1"

case "$PLUTO_VERB" in
    up-client|up-host)
        STATUS=1
        ;;
    down-client|down-host)
        STATUS=0
        ;;
    *)
        exit 0
        ;;
esac

zabbix_sender -z "$ZABBIX_SERVER" -s "$ZABBIX_HOST" \
    -k "vpn.tunnel.status[${PLUTO_CONNECTION}]" \
    -o "$STATUS"

В Zabbix создаём trapper-item на каждое соединение с именем vpn.tunnel.status[имя-соединения] и тригер: если значение 0 дольше 2 минут - алерт. Двухминутный зазор нужен чтобы не шуметь при плановом рестарте туннелей при ротации ключей.

Что получилось по времени переключения

Измеряли несколько раз: жёсткий kill Bird на одном из шлюзов -> BGP-сессия рвётся -> провайдер перестаёт анонсировать /29 через этот аплинк -> трафик уходит на второй шлюз. Время от kill до появления маршрута через второй шлюз - от 2 до 4 секунд в зависимости от настроек holdtime на BGP-пире. Мы выставили holdtime 9 секунд, keepalive 3 секунды - это компромисс между быстрым детектом и количеством служебного трафика.

Сценарий с healthcheck работает медленнее - он срабатывает по cron раз в 30 секунд, то есть в худшем случае шлюз будет в ротации до 30 секунд с мёртвыми туннелями. Для большинства клиентов это приемлемо, но если нужно быстрее - можно запускать healthcheck через systemd timer с меньшим интервалом или добавить updown-скрипт StrongSwan, который при падении туннелей сразу дёргает birdc disable.

Что не решено

Active-Active означает, что одна IKE-сессия пользователя привязана к конкретному шлюзу - переключение всё равно разрывает сессию. Для road-warrior с современными клиентами (NetworkManager, встроенный IKEv2 в Windows 10) это переустановка за несколько секунд, пользователь почти не замечает. Но если у кого-то старый клиент с долгими таймаутами переподключения - будет заметнее.

Синхронизация состояния IKE-сессий между шлюзами (чтобы при переключении не нужно было переустанавливать сессию) - задача принципиально другого уровня сложности. StrongSwan умеет HA-кластеризацию через плагин ha, но мы его не тестировали в продакшне. Пока клиента устраивает переподключение.

В рамках managed-сопровождения эта конфигурация сейчас мониторится: Bird-процесс, BGP-пиры, туннели через Zabbix trapper. Следующий шаг - добавить автоматическую проверку симметрии: убедиться что оба шлюза анонсируют /29 одновременно, и алертить если один из них тихо выпал из BGP без срабатывания healthcheck.

Контакт

Нужна такая же инженерная работа?

Опишите задачу и контекст. Ответим в течение рабочего дня, при необходимости подпишем NDA.