Очередная всратая история про Tailscale.
Была задача: заменить один легаси Tailscale узел нормальной HA парой.
Два EC2 в Auto Scaling Group, два AZ, автоматическая ротация auth-key через Lambda + Vault.
Красиво, надёжно, как в книжках и доке написано.
Задеплоили, новые ноды поднялись, анонсируют маршруты, всё выглядит отлично.
Убрали легаси (просто потушили).
Через некоторое время в слаке:
- "а почему внутренний адрес ArgoCD отдаёт 403? э!"
Начинаем смотреть чо там: ДНС не резолвит внутренние имена, весь туннель работает, TCP через
/16 ходит нормально, но стоит обратиться по FQDN к любому внутреннему сервису - тишина.dig +short internal-service.company.example.com
;; connection timed out; no servers could be reached
Ну окей, DNS сломался, а почему.
В AWS VPC есть такая штука -
AmazonProvidedDNS. Это встроенный резолвер, который живёт по адресу <CIDR_VPC_BASE>+2. Если мой VPC это
10.72.0.0/20, то резолвер сидит на 10.72.0.2.Через него работает сплит-днс: внутренние имена идут в приватные Route53 зоны, всё остальное - наружу.
Чтобы это работало через Tailscale-туннель, клиенты должны уметь достучаться до
10.72.0.2.Легаси-нода анонсировала два маршрута:
10.72.0.0/16 (широкая подсеть) и 10.72.0.2/32 (конкретно этот адрес резолвера).Новые ноды анонсировали только один маршрут
10.72.0.0/16.Как оказалось (спасибо нейронкам, документации и коллегам) tailscale работает по принципу
Longest Prefix Match (LPM): при выборе маршрута побеждает самый специфичный: - запрос к
10.72.0.2 шёл через легаси, потому что у неё был /32 - он длиннее /16.А потом легаси потушили. И tailscale не переключился на
/16 новых нод.Это не баг, кстати, а документированное поведение, написанное прямо в KB-1019:
"Tailscale does not fall back to a less-specific route when the subnet router for a more-specific route goes offline.
Tailscale drops traffic to 10.0.0.1 rather than falling back to subnet router A's 10.0.0.0/16 route."
Я, конечно, как и коллеги, прочитали это уже после. Ну всё как обычно.
Для работы HA failover у tailscale вообще требуется, чтобы обе ноды анонсировали идентичные префиксы - широкий
/16 не является фолбэком для /32 - это просто другой маршрут.Итого: DNS-резолвер 10.72.0.2 видел только легаси через
/32, новые ноды с 10.72.0.0/16 для tailscale были вообще не кандидатами на трафик к этому конкретному адресу. Убрали легаси - адрес пропал из таблицы маршрутов. DNS умер и всё, издец.TCP до других адресов в
10.72/16 работало нормально - там /32-специфики не было, оба маршрута от новых нод были равнозначными, failover сработал.Фикс простой: каждая нода должна явно анонсировать
<VPC_CIDR_BASE>+2/32.В terra модуле добавили вычисление адреса резолвера через
cidrhost(var.vpc_cidr, 2) и автоматически подклеиваем его как /32 к списку advertised_routes. Плюс в tailscale ACL нужна отдельная запись в
autoApprovers.routes под этот /32 - общий /16 его не покрывает, там тоже exact match 😬.locals {
vpc_dns_resolver = cidrhost(var.vpc_cidr, 2)
advertised_routes = distinct(
concat(var.advertised_routes, ["${local.vpc_dns_resolver}/32"])
)
}Теперь при любой замене ноды все HA-пиры анонсируют и
/16, и /32 резолвера - файловер работает корректно, ДНС не падает.Мораль тут банальная, но всё равно немного обидная: читай документацию до деплоя, а не после инцидента.
Мы предполагали, что Tailscale при отсутствии
/32 анонсера переключится на покрывающий /16. Вроде логично же - подсеть включает адрес, значит маршрут есть, но у tailscale другая логика:
/32 и /16 - это разные маршруты, не уточнение одного другим, фоллбек намеренно не делается, потому что иначе можно случайно отправить трафик не туда.Один 32-битный префикс, пропущенный при проектировании нового модуля - и несколько часов дебага всей командой*.
* я только коммиты смотрел, читал доки тейлскейла, AWS по VPC и слушал на митосе как ребята траблшутят.
- - -
В следующий раз, когда мне захочется поныть про "тупые" вопросы о сетях и подсетях на собесе, просто вспомню, как один /32 положил нам DNS 😬
