Микро-Go-сервис, что же с ним не так?

Samoilenko Yuri
rnds
Published in
4 min readSep 23, 2020
Непоседа (python), Мякиш (ruby) и Нетак (Go)

Когда мы выполняем свою работу, то нам очень удобно работать со “зрелыми” языками и/или фреймворками. Это позволяет сконцентрироваться на “высокоуровневом” решении задач, что, на мой взгляд, самое интересное в нашей работе. В данном случае я говорю о таких приятных и удобных (handy) языках как python или ruby. Языки весьма старые, но динамичные, все стандартные вещи уже решены и часто хорошо. Когда надо что-то сделать они “просто работают”. Постепенно, со временем, начинаешь ко всем языкам относиться как к “просто работающим” и вдруг… Go

Я совсем не считаю Go незрелым языком, но намедни мы столкнулись с рядом неприятных вещей при реализации небольшого сервиса на Go и “осадочек-то остался”. Этот осадочек и подтолкнул меня к написанию заметки.

Итак, начнем мы с TLS.

Потребовалось нам просто подключиться к RabbitMQ с использованием TLS. Но не просто TLS, а с целой иерархией TLS сертификатов, которая выглядит так:

Иерархия TLS сертификатов

Один RabbitMQ, выделенный под проект, куча независимых версий проекта, запущенных параллельно, и надо чтоб это всё могло динамически подниматься, работать и уходить в закат. Такой вот динамичный тестово-производственный стенд.

Особенностью нашей схемы является то, что сервер RabbitMQ знает вышестоящую цепочку сертификатов, но понятия не имеет о сертификатах уровня “Instance CA” и “просто так” проверить сертификат уровня “Client” у него нет возможности. Как обычно люди работают с TLS? Примерно по такой мантре:

Чтобы подключиться к серверу с использованием TLS надо передать в библиотечную функцию свой ключ, сертификат и сертификат центра сертификации, который может быть массивом.

Большинство наших сервисов написано на ruby и там все прекрасно работает, но и на других языках все выглядит почти одинаково:

Python и библиотека pika
Ruby и библиотека bunny

Это работает на ruby, работает на питоне и как-то так естественно получается, что очень хочется, чтоб также работало и на Go:

Go и TLS

Но тут нас ждет пренеприятное разочарование и весьма неприятная отладка, настолько неприятная, что пришлось даже раскопать и изучить спецификации TLS и надругаться над системными библиотеками Go чтоб проверить ряд гипотез и понять что же не так.

Скажут ему: «Сядь!» — он встает. Скажут «Иди!» он стоит. Если хорошо — говорит «плохо», если плохо — говорит «хорошо», и всегда любил приговаривать «не так» да «не так».

А не так оказалось вот что: как клиент, так и сервер могут достраивать цепочку сертификатов во время установления соединения, получая эти самые сертификаты “с той стороны”. Это описано в разделе 7.4.2 спецификации RFC5246, посвященной TLS 1.2:

7.4.2. Server Certificate

Баг это или фича, которая усиливает безопасность, я не знаю. Может тут сказывается то, что экспертиза в глубинных потрохах TLS не особо нужна разработчику, решающему “сложные и интересные задачи”.

Возвращаясь к теме “зрелых” фреймворков, оказалось следующее: ruby работает, python работает, Go — нет. Решение совсем не сложное, но его поиск потребовал огромного количества времени и нервов двух человек, потрошение стандартной библиотеки Go, выполнения тестов на разных языках, изучения стандартов и пр. Вот так в результате выглядит патч:

примерно 10 часов работы

Следующей неожиданностью явилась работа с 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.

Всем добра и до новых встреч!

--

--

Samoilenko Yuri
rnds
Editor for

Lead developer and technical writer @ RNDSOFT // Ruby 💕