docker中的DNS解析问题

诡异报错–DNS解析问题

报错现象

在执行 docker 命令报如下错误

1
2
3
$ docker search centos
Error response from daemon: Get "https://index.docker.io/v1/search?q=centos&n=25": dial tcp: lookup index.docker.io on 192.168.1.1:53: read udp 192.168.1.107:50665->192.168.1.1:53: i/o timeout

经过几次尝试,分析发现如下:

1. 发现的报错的命令都是和联网有关系的命令。
2. 只有docker的命令有问题,centos上其他的curl,wget,yum命令都没问题,
    都能正常访问网络,正常解析域名的。
3. 之前在Ubuntu系统没遇到这种诡异问题。

做过什么尝试

经过分析发现 如果改了 /etc/resolv.conf 加上个 8.8.8.8 的dns站点就能好使了。

1
2
3
4
5
6
7
8
9
10
11
12
$ cat /etc/resolv.conf  
# Generated by NetworkManager
nameserver 192.168.0.1
nameserver 192.168.1.1 # 这个是我的路由器的IP

最后的解决办法就是 想办法改这个文件,可以直接改,也可以其他方式,总之这个问题和dns解析有关,
我是改的路由器里面 DHCP 上的2个dns地址的,之前设置的都是 0.0.0.0

$ cat /etc/resolv.conf
# Generated by NetworkManager
nameserver 223.5.5.5
nameserver 119.29.29.29

差异对比分析

为什么会这样呢?我在Ubuntu 环境下面也没这样。

基本上
Ubuntu上这个文件是这样的,然后 127.0.0.53 这个也是本机ip,
然后本机上开启的有个 53 端口的 domain 服务。怀疑这个服务器就是提供dns解析的。
1
2
3
$ cat /etc/resolv.conf
nameserver 127.0.0.53
options edns0

Ubuntu的 systemd-resolved 将默认监听在53号端口,

1
2
3
4
5
6
7
8
$ sudo  netstat -lnpt|grep 53   
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN 983/systemd-resolve

sudo lsof -i :53
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
systemd-r 983 systemd-resolve 12u IPv4 27226 0t0 UDP localhost:domain
systemd-r 983 systemd-resolve 13u IPv4 27227 0t0 TCP localhost:domain (LISTEN)

centos 上 好像 没有这个 systemd-resolved 服务

如何调试

下载代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14

下载 docker cli 仓库代码
https://github.com/golang108/docker-cli.git

尝试编译docker命令行工具,在如下目录是入口。
docker-cli/cmd/docker、docker.go


下载 dockerd 仓库代码
https://github.com/golang108/docker-moby.git

尝试编译dockerd 仓库代码。
docker-moby/cmd/dockerd。docker.go

调试需要的环境

  1. 准备个 centos 系统。
  2. 准备docker命令
  3. 准备dockerd命令
  4. 准备containerd命令
  5. 准备runc命令
  6. /etc/resolv.conf 文件还原之前报错的那个。

基于以上几个,我们 把工具都复制到centos系统上。

1
2
3
4
$ cat /etc/resolv.conf  
# Generated by NetworkManager
nameserver 192.168.0.1
nameserver 192.168.1.1 # 这个是我的路由器的IP

启动 container 服务

1
2
3
4
## 切换到root账号下面 直接 执行 命令即可,

[root@localhost mamh]# ./containerd

准备runc命令

这个比较简单,直接复制个 runc 命令 过来进行了,放到 /usr/bin/ 等系统环境目录下面 即可。

启动 dockerd 服务

1
2
[root@localhost mamh]# ./dockerd -G mamh --debug --raw-logs

这里加上了 –debug 等调试选项,让打印更多的日志输出。

执行 docker search 命令

执行之后, dockerd 那边 就会发现 如下的一段报错日志。

1
2
3
DEBU[2023-07-15T19:52:23.951931453+08:00] searchRepositories                            url="https://index.docker.io/v1/search?q=x&n=25"
ERRO[2023-07-15T19:52:43.956544773+08:00] Handler for GET /v1.44/images/search returned error: Get "https://index.docker.io/v1/search?q=x&n=25": dial tcp: lookup index.docker.io on 192.168.1.1:53: read udp 192.168.1.102:39461->192.168.1.1:53: i/o timeout

过程分析:

1. 经过 docker 中 search 命令相关源码的 阅读可以知道:执行 ‘docker search x’ 时候其实是 发送了请求到 dockerd 这个进程里面了。cli/command/registry/search.go 相关文件在这里。
调用的 这个 ‘ cli.get(ctx, "/images/search", query, headers) ’ 函数发送的一个get请求。

2. 在dockerd 中 有个这样的路由配置: “		router.NewGetRoute("/images/search", ir.getImagesSearch), ” 相关文件在 api/server/router/image/image.go 中。

3. 这个路由配置里面会调用 getImagesSearch() 函数,相关文件在 api/server/router/image/image_routes.go

4. 后面又经过几个函数的调用,最终调用到searchRepositories 里面的这个“ res, err := r.client.Do(req)”, 这个 Do 方法就来自 http 包下面了

5. http 里面也是调用了  func Dial(network, address string) (Conn, error)去创建一个连接,这个方法里使用了net.Dialer。

6. 最终域名解析在这里 type Dialer struct {结构体里面的  Resolver *Resolver  //DNS解析器

后面经过对 dial.go lookup.go 等文件源码分析,发现了一个调试技巧,

可以设置 GODEBUG 的 netdns 控制部分行为

export GODEBUG=netdns=cgo+9  # cgo 表示 使用 libc 库里面方式解析域名

export GODEBUG=netdns=go+9   # go 表示使用纯粹 的go代码库方式解析域名。centos和ubuntu的差异就是这里导致的。

测试的时候 ubuntu 用了cgo。 centos 用来 go 方式。

cgo 的情况日志输出 :

1
2
3
4
5
6
7
8
9
[root@localhost mamh]# export GODEBUG=netdns=cgo+9
[root@localhost mamh]# ./___go_build_dockerd -G mamh --debug --raw-logs
go package net: confVal.netCgo = true netGo = false
go package net: using cgo DNS resolver
go package net: hostLookupOrder(localhost) = cgo # 这里解析 localhost 域名的调试打印输出


go package net: hostLookupOrder(index.docker.io) = cgo # 这里解析 index.docker.io 域名的调试打印输出
time="2023-07-15T22:10:10.605278120+08:00" level=debug msg="after r.client.Do(req)" res="&{200 OK 200 HTTP/1.1 1 1 map[Content-Length:[81] Content-Type:[application/json] Date:[Sat, 15 Jul 2023 14:10:10 GMT] Strict-Transport-Security:[max-age=31536000] X-Trace-Id:[8b8f23ac18f3c788a52b462b76d0d6ec]] 0xc000f160d8 81 [] true false map[] 0xc00049c800 0xc000af0210}"

go的情况日志输出 :

1
2
3
4
5
6
7
8
9
10
11
12
[root@localhost mamh]# export GODEBUG=netdns=go+9
[root@localhost mamh]#
[root@localhost mamh]# ./___go_build_dockerd -G mamh --debug --raw-logs
go package net: confVal.netCgo = false netGo = true
go package net: GODEBUG setting forcing use of Go's resolver
go package net: hostLookupOrder(localhost) = files,dns # 这里解析 localhost 域名的调试打印输出

go package net: hostLookupOrder(index.docker.io) = files,dns # 这里解析 index.docker.io 域名的调试打印输出
time="2023-07-15T23:09:56.575716547+08:00" level=debug msg="after r.client.Do(req)" res="<nil>"
time="2023-07-15T23:09:56.575826290+08:00" level=error msg="Handler for GET /v1.44/images/search returned error: Get \"https://index.docker.io/v1/search?q=x&n=25\": dial tcp: lookup index.docker.io on 192.168.1.1:53: read udp 192.168.1.102:53988->192.168.1.1:53: i/o timeout"


Golang DNS解析

https://zhuanlan.zhihu.com/p/54989059

相关源码文件

1
2
3
go1.19.1/src/net/dial.go

go1.19.1/src/net/lookup.go

大致流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
net.Dial("tcp", service)


d.Dial(network, address) # d是 *Dialer 类型

d.DialContext(context.Background(), network, address)

d.resolver().resolveAddrList(resolveCtx, "dial", network, address, d.LocalAddr)

r.internetAddrList(ctx, afnet, addr) # r是 r *Resolver 类型, afnet = tcp, addr=www.baidu.com:443

host, port, err = SplitHostPort(addr) # 经过 这个切分 host 和 port了

ips, err := r.lookupIPAddr(ctx, net, host) # Try as a literal IP address, then as a DNS name., 这个执行完就找到 域名对应的ip 了。

resolverFunc := r.lookupIP 这个就是 lookup_unix.go中的 lookupIP 函数

lookupIP 函数

lookup_unix.go 中的 lookupIP 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14

func (r *Resolver) lookupIP(ctx context.Context, network, host string) (addrs []IPAddr, err error) {
if r.preferGo() { # 这里是false,不会进入的
return r.goLookupIP(ctx, network, host)
}

order := systemConf().hostLookupOrder(r, host) # 这里选择了 hostlooupCgo 这个值, systemConf() 中 是调用初始化 initConfVal()函数的。里面可以设置 godebug.Get("netdns")

if order == hostLookupCgo {
if addrs, err, ok := cgoLookupIP(ctx, network, host); ok { # 直接进入这里,采用 cgoLookupIP 去解析了。。。。。。。
return addrs, err
}
。。。。
}

cgoLookupIP 函数

cgo_unix.go 中的 cgoLookupIP 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

func cgoLookupIP(ctx context.Context, network, name string) (addrs []IPAddr, err error, completed bool) {
if ctx.Done() == nil {
addrs, _, err = cgoLookupIPCNAME(network, name)
return addrs, err, true
}
result := make(chan ipLookupResult, 1)
go cgoIPLookup(result, network, name)
select {
case r := <-result:
return r.addrs, r.err, true
case <-ctx.Done():
return nil, mapErr(ctx.Err()), false
}
}

cgoLookupIPCNAME 函数

1
2
func cgoLookupIPCNAME(network, name string) (addrs []IPAddr, cname string, err error) {

C.getaddrinfo 函数, 这个就调用到了 c 库里面的函数啦。。。。

1
gerrno, err := C.getaddrinfo((*C.char)(unsafe.Pointer(&h[0])), nil, &hints, &res)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <unistd.h>
#include <string.h>

// If nothing else defined EAI_OVERFLOW, make sure it has a value.
#ifndef EAI_OVERFLOW
#define EAI_OVERFLOW -12
#endif
*/
import "C"

golang中 DNS 解析分类

大致分类如下 几个类型

参考 net/dnsclient_unix.go 文件 中定义的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const (
// hostLookupCgo means defer to cgo.
hostLookupCgo hostLookupOrder = iota
hostLookupFilesDNS // files first
hostLookupDNSFiles // dns first
hostLookupFiles // only files
hostLookupDNS // only DNS
)

var lookupOrderName = map[hostLookupOrder]string{
hostLookupCgo: "cgo",
hostLookupFilesDNS: "files,dns",
hostLookupDNSFiles: "dns,files",
hostLookupFiles: "files",
hostLookupDNS: "dns",
}

上面的报错原因 整体分析下来,发现 Ubuntu 里面 用了 hostLookupCgo: "cgo",

而 centos 里面 用了 hostLookupFilesDNS: "files,dns", 具体可以参考 hostLookupOrder() 函数

Ubuntu 下 c.resolv.unknownOpt = true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
dnsReadConfig("/etc/resolv.conf") 因为这个 解析 options edns0 这一行 直接 unknownOpt 设置为 true了。

然后在 hostLookupOrder() 函数里面,直接 提前 判断 返回了 hostLookupCgo 这个类型了。

type dnsConfig struct {
servers []string // server addresses (in host:port form) to use
search []string // rooted suffixes to append to local name
ndots int // number of dots in name to trigger absolute lookup
timeout time.Duration // wait before giving up on a query, including retries
attempts int // lost packets before giving up on server
rotate bool // round robin among servers
unknownOpt bool // anything unknown was encountered
lookup []string // OpenBSD top-level database "lookup" order
err error // any error that occurs during open of resolv.conf
mtime time.Time // time of resolv.conf modification
soffset uint32 // used by serverOffset
singleRequest bool // use sequential A and AAAA queries instead of parallel queries
useTCP bool // force usage of TCP for DNS resolutions
}

Centos 下 case filesSource && dnsSource: 走到这里了,并且 if first == “files”

1
2
3
4
5
6
7
8
9
10
11
// Cases where Go can handle it without cgo and C thread
// overhead.
switch {
case filesSource && dnsSource:
if first == "files" {
print("return hostLookupFilesDNS, ", filesSource, dnsSource, first)
return hostLookupFilesDNS
} else {
return hostLookupDNSFiles
}

总之 centos 代码逻辑 走到了 解析 /etc/nsswitch.conf 判断出来的条件的情况下了。