Увидел недавно замечательный патч, который позволяет не делать два днс запроса (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{Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
d := &net.Dialer{}
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
, но это уже другая история.