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{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 в днс):

root@ubuntuinfra:/tmp# 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, но это уже другая история.