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.