Всем привет! Работая DevOps-инженером, я задумался о мониторинге IPsec-туннелей, которых у нас уже накопилось достаточно. Они в основном используются для связи между облаками, так как инфраструктура разнесена — например, dev и prod живут у разных облачных провайдеров. Также есть интеграции со сторонними организациями, кластеры Kubernetes в AWS, GCP и т.д. Основная цель — получать алерты о падении туннеля раньше, чем сработают алерты о недоступности сервисов. Это особенно важно, поскольку Prometheus у нас один, он живёт в одном из облаков, а prometheus-stack в Kubernetes-кластерах работают в режиме агентов.
Первая проблема — выбор экспортера или разработка своего
Изначально наткнулся на экспортер от dennisstritzke, но проект уже архивный, последний релиз датируется сентябрем 2021 года, в README автор рекомендует использовать более свежий и поддерживаемый экспортер. Однако он использует VICI, соответственно необходима миграция с более старого подхода конфигурирования с помощью ipsec.conf на swanctl.conf. В документации есть подробное описание, и даже ссылка на скрипт-конвертор. Но зачем ломать то, что уже работает, пусть даже и deprecated? В итоге написал свой python скрипт, который дергает ipsec status, парсит вывод и формирует необходимые мне метрики для Prometheus.
app.py
Скрытый текст
#!/usr/bin/env python3
import time
import logging
import subprocess
from prometheus_client import start_http_server, Gauge
import re
# Настройка логирования
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
handlers=[
logging.StreamHandler()
]
)
# Инициализация метрик
UP_TUNNELS = Gauge('ipsec_up_tunnels', 'Number of active IPsec tunnels')
CONNECTING_TUNNELS = Gauge('ipsec_connecting_tunnels', 'Number of connecting IPsec tunnels')
def get_tunnel_metrics():
"""Получаем количество активных и подключающихся туннелей StrongSwan через ipsec status."""
try:
logging.info("Выполнение команды `ipsec status` для получения состояния туннелей.")
# Выполнение команды ipsec status
result = subprocess.run(
["ipsec", "status"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
if result.returncode != 0:
logging.error(f"Ошибка при выполнении команды `ipsec status`: {result.stderr.strip()}")
UP_TUNNELS.set(0)
CONNECTING_TUNNELS.set(0)
return
status_output = result.stdout
if not status_output.strip():
logging.warning("Вывод команды `ipsec status` пустой. Устанавливаю метрики в 0.")
UP_TUNNELS.set(0)
CONNECTING_TUNNELS.set(0)
return
# Ищем строку "Security Associations (X up, Y connecting)"
sa_match = re.search(r"Security Associations ((d+) up, (d+) connecting):", status_output)
if sa_match:
up_tunnels = int(sa_match.group(1))
connecting_tunnels = int(sa_match.group(2))
logging.info(f"Активные туннели (up): {up_tunnels}, подключающиеся туннели (connecting):"
f" {connecting_tunnels}")
else:
logging.warning("Не удалось найти строку Security Associations. Устанавливаю метрики в 0.")
up_tunnels = 0
connecting_tunnels = 0
# Обновляем метрики
UP_TUNNELS.set(up_tunnels)
CONNECTING_TUNNELS.set(connecting_tunnels)
except Exception as ee:
logging.exception(f"Ошибка при сборе метрик: {ee}")
UP_TUNNELS.set(0)
CONNECTING_TUNNELS.set(0)
if __name__ == '__main__':
port = 9641
try:
logging.info(f"Запуск IPSec Exporter на порту {port}")
start_http_server(port)
logging.info(f"HTTP-сервер успешно запущен на порту {port}")
except Exception as e:
logging.exception(f"Не удалось запустить HTTP-сервер на порту {port}: {e}")
exit(1)
# Основной цикл опроса метрик
while True:
try:
get_tunnel_metrics()
except Exception as e:
logging.exception(f"Неожиданная ошибка в основном цикле: {e}")
time.sleep(5) # Опрос каждые 5 секунд
Dockerfile:
Скрытый текст
FROM python:3.9-slim
WORKDIR /usr/local/bin
RUN pip install --no-cache-dir prometheus-client
RUN apt-get update && apt-get install -y --no-install-recommends
strongswan
&& rm -rf /var/lib/apt/lists/*
COPY docker/ipsec-exporter/app.py .
RUN chmod +x app.py
EXPOSE 9641
CMD ["app.py"]
docker-compose.yaml
Скрытый текст
services:
ipsec_exporter:
image: prometheus_exporters/ipsec-exporter:latest
restart: unless-stopped
pid: "host"
ports:
- "9641:9641"
volumes:
- /var/run/starter.charon.pid:/var/run/starter.charon.pid
- /var/run/charon.pid:/var/run/charon.pid
- /var/run/charon.ctl:/var/run/charon.ctl
И вроде всё хорошо, метрики в Prometheus прилетают, настроили алерты:
Скрытый текст
- alert: NoActiveIPSecTunnels
expr: ipsec_up_tunnels == 0
for: 1m
labels:
severity: average
annotations:
summary: "Нет активных туннелей IPsec"
description: "Все туннели IPsec неактивны в течение минуты."
- alert: TooManyConnectingIPSecTunnels
expr: ipsec_connecting_tunnels > 1
for: 30s
labels:
severity: warning
annotations:
summary: "Не все ipsec туннели в статусе up"
description: "Количество подключающихся туннелей IPsec {{ $value }}."
Но сама идея пробрасывать через volumes сокет charon в контейнер мягко говоря не очень, потому как при выполнении команды ipsec restart на хостовой машине связь с сокетом пропадала и не восстанавливалась, что логично. Привожу подробные конфиги для тех кто захочет повторить нечто подобное, возможно для других целей. Дорабатывать скрипт, писать какие-то дополнительные, костыльные решения не было желания, поэтому от собственного экспортера быстро отказались. Решили таки переписать конфиги туннелей и использовать готовый экспортер.
Вторая проблема — лаконичность конфигов или «те же яйца, только в профиль»
Для миграции с ipsec.conf
на swanctl.conf
первым делом я попробовал использовать скрипт-конвертер. Я добавил его в PyCharm, создал необходимые директории и файлы. Скрипт отработал, но на выходе я не получил ожидаемого результата. Видимо, требовался рефакторинг кода или использование более старых версий Python. Конечно, самый правильный подход это использование документации и самостоятельное формирование конфигов, но это занимает уж очень много времени. В итоге за основу была взята статья неизвестного мне автора. Привожу пример одного из своих старых конфигов и то, что получилось при его миграции:
Старый подход - /etc/ipsec.conf
Скрытый текст
# ipsec.conf - strongSwan IPsec configuration file
config setup
charondebug="all"
uniqueids=yes
strictcrlpolicy=no
conn tun-to-rogaikopyta
authby=secret
left=%defaultroute
leftid=my_public_ip
leftsubnet=my_subnet
right=remote_public_ip
rightid=remote_public_ip
rightsubnet=remote_subnet
ike=aes256-sha1-modp1024
esp=aes256-sha1-modp1024
keyingtries=1
leftauth=psk
rightauth=psk
keyexchange=ikev1
ikelifetime=24h
lifetime=1h
auto=route
conn tun-to-rogaikopyta-2
also=tun-to-rogaikopyta
leftsubnet=my_subnet
rightsubnet=remote_subnet1
conn tun-to-rogaikopyta-3
also=tun-to-rogaikopyta
leftsubnet=my_subnet
rightsubnet=remote_subnet2
conn tun-to-rogaikopyta-4
also=tun-to-rogaikopyta
leftsubnet=my_subnet
rightsubnet=remote_subnet3
...
Новый подход - /etc/swanctl/conf.d/ipsec.conf
Скрытый текст
connections {
tun-to-rogaikopyta {
version = 1
local_addrs = my_private_ip
remote_addrs = remote_public_ip
proposals = aes256-sha1-modp1024
keyingtries = 1
local {
auth = psk
id = my_public_ip
}
remote {
auth = psk
id = remote_public_ip
}
children {
tun-to-rogaikopyta-1 {
mode = tunnel
local_ts = my_subnet
remote_ts = remote_subnet1
start_action = trap
esp_proposals = aes256-sha1-modp1024
}
tun-to-rogaikopyta-2 {
mode = tunnel
local_ts = my_subnet
remote_ts = remote_subnet2
start_action = trap
esp_proposals = aes256-sha1-modp1024
}
tun-to-rogaikopyta-3 {
mode = tunnel
local_ts = my_subnet
remote_ts = remote_subnet3
start_action = trap
esp_proposals = aes256-sha1-modp1024
}
...
Одним из основных преимуществ перехода на новую модель конфигурирования ipsec туннелей многие называют лаконичность конфигов. С одной стороны это правда, ведь мы могли сделать так:
Скрытый текст
connections {
tun-to-rogaikopyta {
version = 1
local_addrs = my_private_ip
remote_addrs = remote_public_ip
proposals = aes256-sha1-modp1024
keyingtries = 1
local {
auth = psk
id = my_public_ip
}
remote {
auth = psk
id = remote_public_ip
}
children {
tun-to-rogaikopyta-1 {
mode = tunnel
local_ts = my_subnet
remote_ts = remote_subnet1, remote_subnet2, remote_subnet3
start_action = trap
esp_proposals = aes256-sha1-modp1024
В моём случае это бы сильно помогло, но есть одно но, с другой стороны Cisco ASA. Она принадлежит сторонней компании, доступа к ней у меня нет. А просить тамошних сетевых инженеров перенастроить туннели с их стороны, потому что я гонюсь за лаконичностью такое себе. Новый конфиг был протестирован в тестовом окружении, для этого пришлось развернуть в облаке две машины, две VPC, две таблицы маршрутизации и т.д. После тестирования, в не рабочее время было организовано переключение.
Третья проблема - скупой README.md в проектах
После успешного поднятия туннелей на одном из серверов, решил запустить экспортер и посмотреть какие метрики он отдаёт. В README проекта на github есть пример запуска экспортера в docker:
docker run -it -p 8079:8079 -v $(pwd)/my-config.yaml:/config.yaml --rm torilabs/ipsec-prometheus-exporter:latest
Удивление вызвало то, что при внесении правок в config.yaml и перезапуска контейнера ничего не менялось, потому что в entrypoint не было упоминаний о config.yaml.
По умолчанию сокет vici в Ubuntu имеет следующий адрес socket = unix://var/run/charon.vici
. А экспортер может подключаться по tcp, либо по udp. Для того чтобы заставить его работать по tcp привёл конфиги strongswan к следующему виду:
Скрытый текст
# cat /etc/strongswan.d/charon/vici.conf
vici {
# Whether to load the plugin. Can also be an integer to increase the
# priority of this plugin.
load = yes
# Socket the vici plugin serves clients.
# socket = unix://var/run/charon.vici
socket = tcp://127.0.0.1:4502
}
# cat /etc/strongswan.d/swanctl.conf
swanctl {
# Plugins to load in swanctl.
# VICI socket to connect to by default.
# socket = unix://var/run//charon.vici
socket = tcp://127.0.0.1:4502
}
# cat /etc/strongswan.conf
# strongswan.conf - strongSwan configuration file
#
# Refer to the strongswan.conf(5) manpage for details
#
# Configuration changes should be made in the included files
charon {
load_modular = yes
plugins {
include strongswan.d/charon/*.conf
}
}
include strongswan.d/*.conf
Так как vici теперь работает на localhost хостовой машины, будем запускать docker контейнер с параметром network_mode: "host", финальный конфиг экспортера и docker-compose.yaml будет выглядеть следующим образом:
Скрытый текст
# cat config.yaml
# Logger configuration
logging:
level: DEBUG
# HTTP server configuration
server:
port: 8079
# Vici configuration
vici:
network: "tcp"
host: "127.0.0.1"
port: 4502
# cat docker-compose.yml
services:
ipsec-exporter:
network_mode: "host"
image: torilabs/ipsec-prometheus-exporter:v0.2.1
command: ["--config=/config.yaml"]
restart: always
volumes:
- ./config.yaml:/config.yaml
Проверить метрики можно командой - curl http://localhost:8079/metrics
Далее осталось сделать дашборд в Grafana и настроить аллертинг, но это уже тема для отдельной статьи. За код на python, орфографию, пунктуацию и network_mode: "host" в docker-compose прошу сильно не пинать :).
Автор: trabl