Микро-Go-сервис, что же с ним не так?
Когда мы выполняем свою работу, то нам очень удобно работать со “зрелыми” языками и/или фреймворками. Это позволяет сконцентрироваться на “высокоуровневом” решении задач, что, на мой взгляд, самое интересное в нашей работе. В данном случае я говорю о таких приятных и удобных (handy) языках как python или ruby. Языки весьма старые, но динамичные, все стандартные вещи уже решены и часто хорошо. Когда надо что-то сделать они “просто работают”. Постепенно, со временем, начинаешь ко всем языкам относиться как к “просто работающим” и вдруг… Go
Я совсем не считаю Go незрелым языком, но намедни мы столкнулись с рядом неприятных вещей при реализации небольшого сервиса на Go и “осадочек-то остался”. Этот осадочек и подтолкнул меня к написанию заметки.
Итак, начнем мы с TLS.
Потребовалось нам просто подключиться к RabbitMQ с использованием TLS. Но не просто TLS, а с целой иерархией TLS сертификатов, которая выглядит так:
Один RabbitMQ, выделенный под проект, куча независимых версий проекта, запущенных параллельно, и надо чтоб это всё могло динамически подниматься, работать и уходить в закат. Такой вот динамичный тестово-производственный стенд.
Особенностью нашей схемы является то, что сервер RabbitMQ знает вышестоящую цепочку сертификатов, но понятия не имеет о сертификатах уровня “Instance CA” и “просто так” проверить сертификат уровня “Client” у него нет возможности. Как обычно люди работают с TLS? Примерно по такой мантре:
Чтобы подключиться к серверу с использованием TLS надо передать в библиотечную функцию свой ключ, сертификат и сертификат центра сертификации, который может быть массивом.
Большинство наших сервисов написано на ruby и там все прекрасно работает, но и на других языках все выглядит почти одинаково:
Это работает на ruby, работает на питоне и как-то так естественно получается, что очень хочется, чтоб также работало и на Go:
Но тут нас ждет пренеприятное разочарование и весьма неприятная отладка, настолько неприятная, что пришлось даже раскопать и изучить спецификации TLS и надругаться над системными библиотеками Go чтоб проверить ряд гипотез и понять что же не так.
Скажут ему: «Сядь!» — он встает. Скажут «Иди!» он стоит. Если хорошо — говорит «плохо», если плохо — говорит «хорошо», и всегда любил приговаривать «не так» да «не так».
А не так оказалось вот что: как клиент, так и сервер могут достраивать цепочку сертификатов во время установления соединения, получая эти самые сертификаты “с той стороны”. Это описано в разделе 7.4.2 спецификации RFC5246, посвященной TLS 1.2:
Баг это или фича, которая усиливает безопасность, я не знаю. Может тут сказывается то, что экспертиза в глубинных потрохах TLS не особо нужна разработчику, решающему “сложные и интересные задачи”.
Возвращаясь к теме “зрелых” фреймворков, оказалось следующее: ruby работает, python работает, Go — нет. Решение совсем не сложное, но его поиск потребовал огромного количества времени и нервов двух человек, потрошение стандартной библиотеки Go, выполнения тестов на разных языках, изучения стандартов и пр. Вот так в результате выглядит патч:
Следующей неожиданностью явилась работа с DNS.
Не буду описывать весь когнитивный диссонанс который пришлось пережить, перейду сразу к результату. Go имеет собственную реализацию процесса разрешения DNS-записей. На практике это означает следующее: если вы пингуете какое-то имя с помощью curl или dig, это вовсе не значит что Go-приложение запущенное в этой же консольке в следующей строчке разрезолвит имя так же. Очень, очень неприятная ситуация когда годами отлаженные инструменты дают железный результат, а рантайм сервиса ведет себя как яркая и индивидуальная личность.
Исправить ситуацию удалось двумя независимыми и странными для стороннего наблюдателя способами.
Первый способ— использовать переменную окружения управления внутренним рантаймом Go:
export GODEBUG=netdns=go # force pure Go resolver
export GODEBUG=netdns=cgo # force cgo resolver
Второй способ — управление приоритетом источников диспетчера службы имён. Хотя тут никакого управления нет, есть мантра и заведенные на гитхабе ишьюсы:
echo 'hosts: files dns' > /etc/nsswitch.conf
Что в итоге?
Конечно, в чужой монастырь со своим самоваром не ходят, но когда есть интересная задача сделать что-то полезное — меньше всего хочется залипать на первом же шаге и с головой погружаться в детали протокола TLS.
Всем добра и до новых встреч!