Go, DNS, и корпоративный VPN: сага о бутылочном горлышке

Вас ждет захватывающая история того, как несчастный девопс копался в исходниках всего, что подвернется под руку, просто чтобы заработала одна команда. В посте прячутся костыли, добро, и целая масса ковролина. Чтобы фантасмагория была понятнее, поясню: все события происходили на macOS (darwin/amd64).

Введение (издалека)

Мне нравится OpenStack. Хорошая система, настоящая операционная система для датацентра (куберы рядом с ним ничего не стоят). Как и положено настоящей операционной системе, тем более для вычислительного кластера, она состоит из большого числа компонентов, и постижение ее требует свитера и бороды. Один из ее компонентов - OpenStack Magnum, штука, создающая кластеры k8s одной командой. Причем, как и положено куберам, которые предоставляются облачным провайдером (OpenStack - это облако, просто добавь своей воды), кластер не простой, а с интеграциями. Хранилище с помощью Cinder, балансировщики нагрузки с помощью Neutron, и, что самое важное для нашего рассказа - авторизация с помощью Keystone, то есть управлять доступом пользователей в кластер можно родным функционалом OpenStack. Чтобы это работало на клиенте, нужен соответствующая утилита командной строки - client-keystone-auth. Я охотно настроил это в своих кластерах, затюнил свой ~/.kube/config, и вместо того, чтобы все обладали админскими правами, пользуясь одним общим kubeconfig1, каждый радостно ходит под своими кредами. Был бы еще не Keystone, а Active Directory (мы же ынтерпраз), вообще была бы сказка. Так как наши кластеры работают по принципу “настроил и забыл”, я не пользовался этим несколько месяцев.

Малец подкрался незаметно

В силу <текущих событий>, работа происходит из дома, по корпоративному VPN. При работе из дома случилась странная вещь - та хваленая авторизация в кластер через Keystone перестала работать, выкидывая ошибку dial tcp: i/o timeout. Соединение не могло установиться, хотя старый добрый cURL проблем с подключением не видел. Странно, но так как все деплои были настроены через CI/CD, и ничего не падало, желания что-то с этим делать особо не было. Наверное, у go был какой-то баг с работой во внутренней сети. Достали пыльные админские kubeconfig, и продолжаем работу.

Сенпай не мог не заметить

Еще один день, еще одна настройка личного кластера в Docker For Mac2. Надо настроить Nginx Ingress Controller. Для установки (настраивать, в общем-то, ничего не надо) есть простая команда:

$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v0.35.0/deploy/static/provider/cloud/deploy.yaml
Unable to connect to the server: dial tcp: i/o timeout

Как говорит Никита Инстаграмщик, опачки. Это уже переходит границы разумного. Сервера гитхаба - публичная сеть, как дотуда-то не достучаться? Пришло время надеть костюм ассенизатора3 и погрузиться в исходники. Открываем репо kubectl4, и начинаем внимательно изучать. Естественно, залезаем в pkg/cmd/apply/apply.go, внимательно пролистываем исходники и распутываем стек. Оказываемся мы совершенно в другом репозитории, на строчке resp, err := http.Get(url). То есть в итоге дергается стандартный метод стандартной библиотеки без каких-либо прикрас.

Воспроизведем проблему?

Как известно, без веских оснований поносить стандартную библиотеку языка, да и любой другой софт - моветон. Надо понять, что вызывает это. Что нам стоит, проект на го построить. Да какой!

$ cat main.go
package main

import (
	"net/http"
)

func main() {
	conn, err := http.Get("https://raw.githubusercontent.com")
	if err != nil {
		panic(err)
	}
	conn.Close()
}

$ go run main.go || echo failed
$

Все работает, почему у них не так? Пришло время для тяжелого вооружения - слушать системные вызовы. Ах да, это же macOS, и на ней что-то потрассировать та еще боль. Ну, как известно, искать лучше где светло. Берем lsof и смотрим, где же утилита зависает:

$ lsof -p 31464
COMMAND   PID  USER   FD     TYPE             DEVICE   SIZE/OFF                NODE NAME
kubectl 31464 anton  cwd      DIR                1,5        416             4010040 /Users/anton/Documents/dev/
kubectl 31464 anton  txt      REG                1,5   49469232             9837065 /Applications/Docker.app/Contents/Resources/bin/kubectl
kubectl 31464 anton  txt      REG                1,5    1558736 1152921500311885591 /usr/lib/dyld
kubectl 31464 anton    0u     CHR               16,5 0t14303394                2783 /dev/ttys005
kubectl 31464 anton    1u     CHR               16,5 0t14303394                2783 /dev/ttys005
kubectl 31464 anton    2u     CHR               16,5 0t14303394                2783 /dev/ttys005
kubectl 31464 anton    3     PIPE 0x60cd7d7cef790dc0      16384                     ->0xe37ce7275a1d221a
kubectl 31464 anton    4     PIPE 0xe37ce7275a1d221a      16384                     ->0x60cd7d7cef790dc0
kubectl 31464 anton    5u    IPv4 0xe39ef2152b07d37f        0t0                 TCP localhost:52420->localhost:sun-sr-https (ESTABLISHED)
kubectl 31464 anton    6u  KQUEUE                                                   count=0, state=0xa
kubectl 31464 anton    7r     CHR               14,1     0t4096                 586 /dev/urandom
kubectl 31464 anton    8u    IPv4 0xe39ef214ee781477        0t0                 UDP 192.168.1.49:52793->ns3.corbina.net:domain
kubectl 31464 anton    9u    IPv4 0xe39ef214ee77fa4f        0t0                 UDP 192.168.1.49:57961->ns3.corbina.net:domain

Interessant. Умирает на UDP запросах к провайдеру. DNS умер? Но операционная система работает исправно, сеть есть, сайты открываются. Что же это может быть? Ручной nslookup так же отказывается работать с публичными DNS серверами. VPN? Открываем таблицу роутинга и внимательно высматриваем, что же пошло не так. Нет, IP-адреса DNS не роутятся в интранет. Может, у Go какой-то хитрый резолвер? Да нет, ведь свой доброскрипт работает…

На каждый хитрый ларчик найдется своя монтировка

Golang имеет два способа резолвить доменные имена:

  1. Резолвер здорового человека: getaddrinfo(3), getnameinfo(3). Требует отдельный тред для резолва.
  2. Резолвер курильщика, который решил, что тратить целый тред плохо, и заимплементил свой резолвер на чистом го. Молодец! Кажется, я нашел, где бутылочное горлышко.

Но ладно, полили дизайнеров Go грязью5, надо добавить ложку меда: резолвер настраивается через переменную окружения прямо в рантайме! GODEBUG=netdns=cgo лечит почти любой вид рака и работает в сложных условиях VPN. И действительно:

$ GODEBUG=netdns=go go run main.go || echo failed
Unable to connect to the server: dial tcp: i/o timeout
failed
$

$ GODEBUG=netdns=cgo go run main.go || echo failed
$

Ну что, теперь, наверное, можно просто выставить эту переменную окружения kubectl, и все заработает?

$ GODEBUG=netdns=cgo kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v0.35.0/deploy/static/provider/cloud/deploy.yaml
Unable to connect to the server: dial tcp: i/o timeout

Отчаяние

Стоковые kubectl, keystone-auth-client, и многие другие тулы, доступные на мак, собираются с флагом CGO_ENABLED=false, зачем им зря рантайм засорять? Что делать, теперь у меня есть любовно собранные своими руками утилитки на го, с вкоряченными в произвольные места флагами компиляции. Я буду дома вовремя!

Погоди, а что же не так с VPN?

Cisco AnyConnect - очень хитрый VPN. Все DNS-запросы, сделанные системными вызовами, перехватываются и направляются куда надо. Все случайные UDP-запросы на 53 порт просто дропаются. Отсюда попытка написать свой резолвер обречена на неудачу. Почему они заодно не изменяют системный список DNS сервера остается загадкой. Можно сделать это за них, и прописать у себя внутренние DNS. Если компьютер рабочий, это не так страшно. Если же компьютер свой, то не забывайте: Иисус страдал, и тебе завещал.

  1. Magnum умеет подписывать админские сертификаты для доступа в кластер. Позволяет логиниться в любых условиях, насколько бы сильно ни был сломан вебхук для аутентикации/авторизации. 

  2. Черт возьми, куберы, встроенные в Docker For Mac бесподобны. Ни разу не возникло проблемы, что чарт не вставал, или не хотел что-то делать. Совершенство из коробки. 

  3. Кого я обманываю, я этот костюм и не снимаю. 

  4. Ридми не читай @ умом страдай. Этот репозиторий содержит большинство кода kubectl, но не сам клиент. Кто ж знал? 

  5. У всех свои хобби.