pathosd: чего стоило анонсировать адрес, только пока сервис жив
Некоторые сервисы в моей сети работают в нескольких местах сразу — DNS-резолвер на каждом хабе, S3-шлюз больше чем на одной ноде хранилища. Клиенты должны видеть один стабильный адрес, а маршрутизация — отправлять их на ближайший экземпляр, который действительно работает. В маршрутизируемой сети это anycast: каждый экземпляр анонсирует один и тот же адрес по BGP. Загвоздка в слове работает — маршрут понятия не имеет, жив ли сервис за ним.
pathosd закрывает этот разрыв. Это проверка здоровья и источник BGP-маршрутов в одном процессе на Go: он проверяет сервис локально (HTTP, DNS, ping, TCP, UDP, gRPC) и анонсирует адрес, только пока проверка проходит, — или оставляет его с пониженным приоритетом, чтобы предпочитались другие. keepalived/VRRP решает похожую задачу, но в одном L2-сегменте с общим MAC, а не поперёк маршрутизируемой mesh-сети из туннелей.
Что он умеет, рассказано в README. Эта заметка — о том, как он к этому пришёл, и в основном о том, что не заработало с первого раза.
Адреса, которые никуда не уходили
Март 2026. У хабов появились общие anycast-адреса — первым был DNS-резолвер. Каждый хаб вешал их на dummy-интерфейс, а FRR анонсировал в mesh-сеть. Просто — и нормально, пока сервис на одном из хабов не останавливался: маршрут оставался, хаб продолжал притягивать трафик, а трафик уходил в никуда.
Решение очевидно на бумаге — анонсировать, только пока сервис здоров, — и мне не хотелось делить его между демоном маршрутизации и скриптом проверки, которые могут разойтись во мнениях. Поэтому pathosd встраивает GoBGP и делает и то, и другое. Если он умирает, сессии падают и маршруты уходят вместе с ними; адреса стартуют отозванными и появляются только после нескольких удачных проверок подряд.
FRR на той же коробке
Апрель. Естественная схема — pathosd говорит с FRR на том же роутере. Она
так и не устоялась: сначала сессия через loopback-адреса, потом через
отдельные /32 на dummy-интерфейсе — и к концу того же дня я от неё
отказался.
Тогда я перестал с этим бороться: pathosd на каждом хабе пирился с FRR на других хабах, а те принимали от него только префиксы VIP. Это было стабильно. А адрес DNS, который действительно важен, на время вернулся к обычному статическому анонсу. Одну мелочь из того периода я оставил: у pathosd на каждом хабе свой номер AS, так что по AS-пути сразу видно, с какого хаба пришёл анонс.
Один день с BIRD
11 августа. Когда mesh-сеть переехала с FRR на BIRD, я захотел вернуть локальную сессию — VIP должен анонсировать тот хаб, который его реально обслуживает. На это ушёл почти весь день:
- Обе стороны ждут друг друга. pathosd звонил BIRD на 179, а BIRD звонил pathosd на его собственный порт; никто не слушал там, куда звонил другой, и сессия висела в Active.
- GoBGP отказался звонить сам себе. Когда pathosd был активной стороной и на обоих концах один и тот же loopback-адрес, срабатывала защита GoBGP от подключения к самому себе. pathosd стал пассивной стороной, BIRD звонит ему.
- «Invalid next hop». pathosd анонсировал VIP с next hop
127.0.0.1. Теперь он анонсирует LAN-адрес роутера. - «Invalid remote address». Для прямого BGP-соседа BIRD ищет его в
подключённой сети, а loopback ею не является.
multihopпропускает эту проверку; в итоге сессия переехала на второй адрес на LAN-бридже. - «Unreachable». Даже с настоящим next hop BIRD не станет маршрутизировать на собственный адрес и помечает маршрут недостижимым. Я пробовал просто экспортировать VIP как connected-маршрут — откатил, вернул обратно, — но так теряется весь смысл: ни контроля по здоровью, ни возможности уступить.
Прижилось решение в несколько строк фильтра импорта BIRD:
import filter {
if net ~ [ 192.0.2.53/32 ] then { # только наши VIP
bgp_next_hop = net.ip; # next hop = сам VIP
preference = 250; # выше connected-маршрута (240)
accept;
}
reject;
};
Локально BIRD по-прежнему считает маршрут недостижимым, но с preference выше connected-маршрута выбирает его и экспортирует соседям по mesh — а больше от него ничего и не нужно.
После этого адрес DNS вернулся под pathosd и теперь уступает, а не исчезает: когда локальная проверка не проходит, маршрут остаётся с удлинённым AS-путём, так что mesh предпочитает другой хаб, но этот у неё остаётся, если остальные ещё хуже. Удлинение проходит через BIRD без изменений — ни iBGP, ни community не нужны.
На той же неделе pathosd научился HTTP-проверкам через forward proxy,
чтобы общий адрес прокси анонсировался, только пока прокси реально
проксирует. А чтобы видеть, что происходит внутри, я включил встроенный
API GoBGP для CLI gobgp — на localhost по TCP, потому что CLI не умеет
в unix-сокет.
Нода хранилища, отвечавшая не оттуда
Конец апреля и снова октябрь. S3-шлюз работает на обычных нодах хранилища под Debian, поэтому у pathosd появился Debian-пакет, а ноды анонсируют адрес шлюза роутеру хоумлаба с проверкой по TLS.
Сюрприз пришёл позже. Клиент в том же сегменте LAN, что и нода, отправлял запрос на адрес шлюза через роутер, как и положено, — а нода отвечала прямо по сегменту, с адреса шлюза. Клиент видел ответ от адреса, с которым на этом линке не разговаривал, и выбрасывал его. Лечится это на ноде, а не в pathosd: ответы с адреса шлюза уходят через роутер по policy routing.
И одна уборка: pathosd только анонсирует, маршруты ему не нужны, поэтому роутер теперь ничего не экспортирует таким пирам, которые только объявляют.