Чиним резолвинг адресов в VPN-локалке (openconnect) для docker и systemd-resolved
Для подключения к корпоративной сети у нас используется CiscoAnyConnect, работает хорошо, но не с докером. Как только докер пытается приподнять свою сеть, утилитка тут же отрубает VPN и переподключает. От этого докер себя плохо чувствует. Поэтому я решил использовать обычный линуксовый openconnect соместно с NetworkManager.
systemd-resolved
Поставил пакеты network-manager-openconnect network-manager-openconnect-gnome
Настроил соединение и оно даже подключилось. Проблема первая, каждый раз когда я подключался снова, он забывал имя пользователя, что раздражает. Я нашел решение и создал
баг. Решение простое, с консоли задаем свое имя в особом виде nmcli con mod prgcvp vpn.secrets ‘form:main:username=yourName’,’save_passwords=yes’
После чего оно будет запомнено. Да, галочку «запомнить пароль» я ставил и пароль он даже запомнил, но вот в тексте галочки про имя пользователя ничего не сказано, так что он честно его забыл 🙂 Напомню, что настройки менеджера лежат в /etc/NetworkManager/system-connections/.
И если параметры знаешь, то можно и руками отредактировать нужное соединение.
Подключаться стало удобно и возникла вторая проблема, имена ресурсов в VPN сети не разрешаются в адреса DNS сервером. Сервер есть, все настройки на месте, но nslookup someserver.local выдает ошибку, а nslookup someserver.local somednsIP выдет правильный ответ. Странно, подумал я, как так, сервер есть, отвечает, а если его конкретно не указать, ошибка? Ответ оказался прост. Когда systemd-resolved пытается найти адрес сервера по имени, он выступает фасадом для других DNS серверов. Делается это так, ссылка /etc/resolv.conf может указывать на несколько мест:
- /run/systemd/resolve/stub-resolv.conf это опция по умолчанию и этот файл будет содержать примерно такое
nameserver 127.0.0.53
options edns0 trust-ad
search somedomain.local - /run/systemd/resolve/resolv.conf это можно использовать если функционал systemd-resolved чем то не устроил когда он прикидывается DNS сервером 127.0.0.53. В итоге это не пригодилось, так для информации пишу.
Саму ссылку /etc/resolv.conf вы можете сами настроить что бы смотрела в любое из мест.
Так вот, дело в том, что openconnect при подключении к VPN получает таблицу route, DNS сервера, а так же search domain и этот домен от VPN сервера приходил неверный (так неправильно настроен у нас). От сервера приходило somedomain.local, а надо было просто local что бы somesrver.local был распознан.
Когда systemd-resolved прикинулся локальным DNS сервером и через /etc/resolv.conf всех отправил к себе за разрешением имен, логика его работы такова. Для каждого коннекта, которые можно посмотреть командой nmcli connection show (это те коннекты, которые знает NetworkManager) systemd-resolved помнит DNS сервера, которые получил по DHCP. Это можно посмотреть командой:
resolvectl dns Global: Link 6 (docker0): Link 5 (vpn0): 999.999.999.999 999.999.999.999 Link 3 (wlp4s0): 192.168.3.8 Link 2 (enp5s0f2):
Когда в 127.0.0.53 приходит запрос на разрешение имени, systemd-resolved смотрит search domain у каждого из коннектов (эти домены он при подключении от DHCP получил тоже). Домены можно посмотреть командой:
resolvectl domain Global: Link 6 (docker0): Link 5 (vpn0): somedomain.local Link 3 (wlp4s0): ~. Link 2 (enp5s0f2):
Далее имя хоста проверяется на частичное совпадение с теми доменами, которые прикреплены к коннектам и самое длинное совпадение и определяет какой конкретно DNS сервер вызвать. Либо все идет в DNS сервер где search domain «~.»
От сервера компании мне приходил неверный search domain (somedomain.local) для VPN коннекта и потому когда я пытался разрешить адрес someserver.local, systemd-resolved их не мог найти, так как предполагал, что DNS сервера, полученные из этого соединения нужны что бы распознавать имена someserver.somedomain.local. Поправил я это подменив search domain в NetworkManager командой
nmcli connection modify yourConnectionName ipv4.dns-search «local»
Доменов может быть несколько через пробел. В итоге все заработало.
Помимо этого я удалил пакет avahi-daemon, так как службы bonjur, которые обслуживает этот демон по умолчанию тоже резолвятся на домене local, а в нашей сетке именно это имя используется для локальной сети и будут конфликты.
docker
Теперь в хост системе работает резолвинг адресов, но при запуске докера резолвинг локальных адресов в VPN может не работать по прежнему. И тут есть несколько вариантов.
Контейнер запущен с network_mode: host в таком случае будет использоваться для резольвинга то, что лежит в /run/systemd/resolve/resolv.conf и там у меня первый же днс сервер выбирается для резольва local и он для этого не подходит. Итог, не работает. Зато все сервисы хост машины видно из контейнера, что еще и неправильно.
Контейнер запущен с network_mode: bridge с созданием отдельной сети. В таком случае сервисы хоста будет не видно, помимо этого будет использован все тот же не работающий у меня /run/systemd/resolve/resolv.conf
Контейнер запущен без настроек сети и использует дефолтный bridge, созданный докером при инсталляции (docker0). В этом случае используется для резольва имен внутренний докеровский DNS, который судя по всему нормально взаимодействует с systemd-resolved и все резолвит как надо. В /etc/resolv.conf будет такое:
bash-5.0# cat /etc/resolv.conf search local nameserver 127.0.0.11 options ndots:0
Если надо показать сервисы хост машины для доступа из контейнера, то просто запускаем сервисы слушать на docker0 IP и получаем доступ.
ifconfig|grep -n1 docker0 27- 28:docker0: flags=4099 mtu 1500 29- inet 192.168.32.1 netmask 255.255.240.0 broadcast 192.168.47.255
Не забываем открыть порты, например у меня ufw зарезал 8080 и пришлось открывать.
Было бы отлично если бы docker просто использовал systemd-resolved dns stub как в последнем описанном варианте всегда. Но к сожалению это не так. В версии systemd-resolved 248, которая только вышла и в дистрах ее нет в документации есть параметр DNSStubListenerExtra, который может задать адрес где слушать для stub. Подробности тут.
В итоге можно будет указать адрес где слушать не захардкоженный 127.0.0.53, а доступный изнутри докера и все будет работать, но пока нет.
Есть другое решение, когда контейнер пойдет на порт 53 мы будем перебрасывать его запросы в стаб systemd с помощью чего то вроде
socat UDP-LISTEN:53,fork,reuseaddr,bind=yourInterfaceIP UDP:127.0.0.53:53
Соответственно, этот DNS можно указать докеру при старте и весь функционал systemd будет работать. Но не стал это использовать.
В итоге мы получаем работающий VPN на хосте, который резолвит имена в локальной сети, работающий докер, который может резолвить эти адреса как родные.
Форум русскоязычного сообщества Ubuntu
Страница сгенерирована за 0.046 секунд. Запросов: 25.
- Сайт
- Об Ubuntu
- Скачать Ubuntu
- Семейство Ubuntu
- Новости
- Форум
- Помощь
- Правила
- Документация
- Пользовательская документация
- Официальная документация
- Семейство Ubuntu
- Материалы для загрузки
- Совместимость с оборудованием
- RSS лента
- Сообщество
- Наши проекты
- Местные сообщества
- Перевод Ubuntu
- Тестирование
- RSS лента
© 2012 Ubuntu-ru — Русскоязычное сообщество Ubuntu Linux.
© 2012 Canonical Ltd. Ubuntu и Canonical являются зарегистрированными торговыми знаками Canonical Ltd.
Go dns
Увидел недавно замечательный патч, который позволяет не делать два днс запроса (A, AAAA) для go resolver, если вы используете ipv4 only. Решил немного покопать на эту тему.
Теперь при использовании go резолвера (в cgo поддержка уже была), можно сделать запрос только для A:
net.Dial("tcp4", "golang.org:80")
Если вы не знали, в Go делается всегда два запроса в днс: A и AAAA, даже если у вас нет IPv6. Без опций single-request они делаются параллельно, но всё же.
Но внутри исходников http.Transport зашито использование tcp , и прокинуть tcp4 нельзя так просто:
conn, err := t.dial(ctx, "tcp", cm.addr())
Начнём немного издалека. Рекомендую освежить знания как работает dns lookup в Linux.
Go resolvers
В Go можно использовать несколько реализаций резолверов: go и cgo. И какой включится по дефолту не очень очевидно, даже не смотря на офф документацию и даже код.
Т.к. я использую в основном Mac Os и Linux, поэтому будем расматривать только их.
Основное влияние оказывают CGO_ENABLED и опции, используемые в resolv.conf и nsswitch.conf .
Если CGO отключен, то всё просто, используется go резолвер.
| Tables | CGO_ENABLED = 0 | CGO_ENABLED = 1 |
|---|---|---|
| Linux | go | it depends =) |
| MacOs | go | cgo |
Mac Os
На Mac Os по дефолту используется cgo.
Linux
На Linux всё сложнее: зависит от используемых env переменных и опций в resolv.conf и nsswitch.conf .
Будет включаться cgo реализация, если есть опции в resolv.conf кроме ndots:, timeout:, attempts:, rotate, single-requests, single-requests-reopen, use-vc, usevc, tcp . Полный список.
Например, в ubuntu может быть такое: options edns0 trust-ad . С такими опциями включится cgo по дефолту.
Что касается nsswitch.conf. Если файл отсутствует, то включается dns go.
С таким nsswitch включается реализация на go (это дефолт в контейнере ubuntu:focal):
passwd: compat group: compat shadow: compat gshadow: files hosts: files dns networks: files protocols: db files services: db files ethers: db files rpc: db files netgroup: nis
У меня на ubuntu было вот такое: hosts: files dns mymachines . С такой конфигурацией включается cgo. Код проверок опций в nsswitch.
Localhost
Ещё есть особенности с лукапом localhost. До 1.16 гошка пытается разрезолвить localhost. А если стоит ndots: 5 , то получается 10 запросов в днс: 5 A + 5 AAAA. На версиях >= 1.16 уже не будет запросов в dns c существующим /etc/hosts. Всё из-за порядка резолва. Фикс в 1.16.
< 1.16 go package net: hostLookupOrder(localhost) = dns,files >= 1.16 go package net: hostLookupOrder(localhost) = files,dns
Проверка
Для проверки, написал маленькую программу, которая позволяет менять транспорт, где подменяется network с tcp на tcp4, например.
package main import ( "context" "flag" "net" "net/http" ) func main() < defClient := flag.Bool("default", true, "use default http client") url := flag.String("url", "https://golang.org", "url to request") flag.Parse() req, err := http.NewRequest("GET", *url, nil) if err != nil < panic(err) >client := httpClient(*defClient) _, err = client.Do(req) if err != nil < panic(err) >> func httpClient(def bool) http.Client < if def < return *http.DefaultClient >return http.Client return d.DialContext(ctx, "tcp4", addr) >, >> >
Смотреть запросы в днс через tcpdump -i any -nnn port 53 .
По дефолту на моей системе включается cgo реализация для dns.
CGO_ENABLED
$ go version; env CGO_ENABLED=0 GODEBUG=netdns=2 go run main.go go version go1.16.2 linux/amd64 go package net: built with netgo build tag; using Gos DNS resolver go package net: hostLookupOrder(golang.org) = files,dns $ go version; env CGO_ENABLED=1 GODEBUG=netdns=2 go run main.go go version go1.16.2 linux/amd64 go package net: dynamic selection of DNS resolver go package net: hostLookupOrder(golang.org) = cgo
CGO resolver
Проверка дефолтного http-клиента с cgo:
$ go version; env GODEBUG=netdns=2 go run main.go go version go1.16.2 linux/amd64 go package net: dynamic selection of DNS resolver go package net: hostLookupOrder(golang.org) = cgo 21:15:18.603496 IP 127.0.0.1.48420 > 127.0.0.53.53: 56222+ [1au] A? golang.org. (39) 21:15:18.603523 IP 127.0.0.1.48420 > 127.0.0.53.53: 7580+ [1au] AAAA? golang.org. (39) 21:15:18.603613 IP 127.0.0.53.53 > 127.0.0.1.48420: 56222 1/0/1 A 173.194.73.141 (55) 21:15:18.603655 IP 127.0.0.53.53 > 127.0.0.1.48420: 7580 1/0/1 AAAA 2a00:1450:4010:c0d::8d (67)
Проверка другого транспорта http-клиента с cgo (тут уже видно, что только 1 запрос с A):
$ go version; env GODEBUG=netdns=2 go run main.go -default=false go version go1.16.2 linux/amd64 go package net: dynamic selection of DNS resolver go package net: hostLookupOrder(golang.org) = cgo 21:16:31.012379 IP 127.0.0.1.57863 > 127.0.0.53.53: 23654+ [1au] A? golang.org. (39) 21:16:31.012516 IP 127.0.0.53.53 > 127.0.0.1.57863: 23654 1/0/1 A 173.194.73.141 (55)
GO resolver
Проверка дефолтного http-клиента с go (2 запроса в днс), go 1.16:
$ go version; env GODEBUG=netdns=go+2 go run main.go go version go1.16.2 linux/amd64 go package net: GODEBUG setting forcing use of Gos resolver go package net: hostLookupOrder(golang.org) = files,dns 21:17:05.750015 IP 127.0.0.1.59921 > 127.0.0.53.53: 21173+ AAAA? golang.org. (28) 21:17:05.750054 IP 127.0.0.1.49789 > 127.0.0.53.53: 46615+ A? golang.org. (28) 21:17:05.750130 IP 127.0.0.53.53 > 127.0.0.1.59921: 21173 1/0/0 AAAA 2a00:1450:4010:c0d::8d (56) 21:17:05.750174 IP 127.0.0.53.53 > 127.0.0.1.49789: 46615 1/0/0 A 173.194.73.141 (44)
Проверка другого транспорта http-клиента с go (2 запроса в днс), go 1.16:
$ go version; env GODEBUG=netdns=go+2 go run main.go -default=false go version go1.16.2 linux/amd64 go package net: GODEBUG setting forcing use of Gos resolver go package net: hostLookupOrder(golang.org) = files,dns 21:17:33.493103 IP 127.0.0.1.59547 > 127.0.0.53.53: 38730+ AAAA? golang.org. (28) 21:17:33.493150 IP 127.0.0.1.46351 > 127.0.0.53.53: 52762+ A? golang.org. (28) 21:17:33.493277 IP 127.0.0.53.53 > 127.0.0.1.59547: 38730 1/0/0 AAAA 2a00:1450:4010:c0d::8d (56) 21:17:33.493319 IP 127.0.0.53.53 > 127.0.0.1.46351: 52762 1/0/0 A 173.194.73.141 (44)
И вот фикс в tip (1.17). Проверка другого транспорта http-клиента с go (тут уже видно, что 1 запрос A в днс):
$ go version; env GODEBUG=netdns=go+2 go run main.go --default=false go version devel go1.17-6986c02d72 Sat Apr 3 18:16:29 2021 +0000 linux/amd64 go package net: GODEBUG setting forcing use of Gos resolver go package net: hostLookupOrder(golang.org) = files,dns 21:20:23.301542 IP 127.0.0.1.57764 > 127.0.0.53.53: 37635+ A? golang.org. (28) 21:20:23.301658 IP 127.0.0.53.53 > 127.0.0.1.57764: 37635 1/0/0 A 173.194.73.141 (44)
| dns requests | 1.16 | tip (1.17) |
|---|---|---|
| cgo | 2 | 2 |
| go | 2 | 2 |
| cgo custom transport | 1 | 1 |
| go custom transport | 2 | 1 |
Если в resolv.conf стоит ndots:5, то умножайте количество запросов на 5.
Localhost
Без nsswitch
Для примера возьмём систему, где нет файла /etc/nsswitch.conf и валидный resolv.conf для го
go 1.15, cgo resolver:
$ go version; env GODEBUG=netdns=cgo+2 go run /tmp/main.go -url 'http://localhost' go version go1.15.10 linux/amd64 go package net: dynamic selection of DNS resolver go package net: hostLookupOrder(localhost) = cgo 14:52:15.144664 IP 127.0.0.1.47784 > 127.0.0.53.53: 64578+ [1au] A? localhost.searchdomain. (43) 14:52:15.144691 IP 127.0.0.1.47784 > 127.0.0.53.53: 3934+ [1au] AAAA? localhost.searchdomain. (43) 14:52:15.144801 IP 127.0.0.53.53 > 127.0.0.1.47784: 64578 1/0/1 A 10.28.4.30 (59) 14:52:15.145400 IP 127.0.0.53.53 > 127.0.0.1.47784: 3934 0/0/1 (43)
go 1.16, cgo resolver:
$ go version; env GODEBUG=netdns=cgo+2 go run /tmp/main.go -url 'http://localhost' go version go1.16.2 linux/amd64 go package net: dynamic selection of DNS resolver go package net: hostLookupOrder(localhost) = cgo 14:50:58.068336 IP 127.0.0.1.52554 > 127.0.0.53.53: 6755+ [1au] A? localhost.searchdomain. (43) 14:50:58.068348 IP 127.0.0.1.52554 > 127.0.0.53.53: 28769+ [1au] AAAA? localhost.searchdomain. (43) 14:50:58.068538 IP 127.0.0.53.53 > 127.0.0.1.52554: 6755 1/0/1 A 10.28.4.30 (59) 14:50:58.069147 IP 127.0.0.53.53 > 127.0.0.1.52554: 28769 0/0/1 (43)
go 1.15, go resolver:
$ go version; env GODEBUG=netdns=2 go run /tmp/main.go -url 'http://localhost' go version go1.15.10 linux/amd64 go package net: dynamic selection of DNS resolver go package net: hostLookupOrder(localhost) = dns,files 14:40:50.008681 IP 127.0.0.1.43172 > 127.0.0.53.53: 45561+ AAAA? localhost.searchdomain. (32) 14:40:50.008694 IP 127.0.0.1.57945 > 127.0.0.53.53: 52303+ A? localhost.searchdomain. (32) 14:40:50.008896 IP 127.0.0.53.53 > 127.0.0.1.57945: 52303 1/0/0 A 10.28.4.30 (48) 14:40:50.009331 IP 127.0.0.53.53 > 127.0.0.1.43172: 45561 0/0/0 (32)
go 1.16, go resolver:
$ go version; env GODEBUG=netdns=2 go run /tmp/main.go -url 'http://localhost' go version go1.16.2 linux/amd64 go package net: dynamic selection of DNS resolver go package net: hostLookupOrder(localhost) = files,dns # tcpdump empty
| without nsswitch | 1.15 | 1.16 |
|---|---|---|
| cgo | not | not |
| go | not | ok |
C nsswitch
С правильным nsswitch файлом hosts: files dns все варианты не запрашивают dns при запросе localhost.
Итоги
Нужно внимательно проверять в каком окружении запускается код, чтобы не было курьёзов. Например, в образе ubuntu:focal включается go резолвер, а в apline:3.13.4 уже cgo, т.к. там нет файла nsswitch.conf . Так же помним про MacOS, что там при включенном CGO_ENABLED включается cgo реализация.
Ещё лучше проверять через tcpdump какие запросы отправляются в днс. Это особенно актуально для kubernetes с его дефолтными ndots:5 , но это уже другая история.
Как в восемь раз уменьшить количество DNS-запросов в Go

Привет, Хабр. Меня зовут Рустам. Я работаю в Ozon: админю Kubernetes и пишу на Go.
У нас очень много сервисов на Go — их количество исчисляется тысячами. Запускаются они внутри кластеров Kubernetes. А я плотно работаю с Kubernetes и заметил, что при запуске кода внутри Kubernetes для резолва одного адреса делается до десяти DNS-запросов. Это, конечно, влияет на производительность.
Я решил разобраться, как Go делает DNS-запросы. В результате мне удалось уменьшить их количество в наших проектах до одного-двух. Как у меня это получилось и можно ли использовать мой опыт в вашем проекте, я расскажу в статье.
Cgo-резолвер vs Go-резолвер
Начнём немного издалека. Будет здорово, если вы освежите знания о том, как работает DNS Lookup в Linux. В данной статье я будут рассматривать версию Go 1.16.
В Go можно использовать две реализации резолверов: Go и Cgo.
Cgo использует системный резолвер, а Go — резолвер, написанный на Go. Казалось бы, они должны работать одинаково, но на самом деле есть различия. И какой включится по умолчанию, не очень очевидно, даже несмотря на документацию и код.
Различия в работе резолверов как раз и определяют, какое количество запросов будет сгенерировано в том или ином случае. Моей целью было выяснить, в каком случае стоит запустить один резолвер, а в каком — другой, и стараться учитывать это на старте проектов.
Итак, первым делом нам нужно понять, какой резолвер будет использоваться в конкретном случае. Это сильно зависит от среды, в которой запускается приложение, а иногда даже от имени хоста. Я использую преимущественно macOS и Linux, поэтому буду рассматривать только их.
В основном я сталкивался с такими внешними факторами, влияющими на выбор резолвера, как CGO_ENABLED и опции, используемые в resolv.conf и nsswitch.conf. Но не только с ними.
CGO_ENABLED
Tables
CGO_ENABLED = 0
CGO_ENABLED = 1