docker container DNS如何配置

50次阅读
没有评论

共计 9822 个字符,预计需要花费 25 分钟才能阅读完成。

丸趣 TV 小编给大家分享一下 docker container DNS 如何配置,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!

Configure container DNSDNS in default bridge networkOptionsDescription-h HOSTNAME or –hostname=HOSTNAME 在该容器启动时,将 HOSTNAME 设置到容器内的 /etc/hosts, /etc/hostname, /bin/bash 提示中。–link=CONTAINER_NAME or ID:ALIAS 在该容器启动时,将 ALIAS 和 CONTAINER_NAME/ID 对应的容器 IP 添加到 /etc/hosts. 如果 CONTAINER_NAME/ID 有多个 IP 地址?–dns=IP_ADDRESS… 在该容器启动时,将 nameserver IP_ADDRESS 添加到容器内的 /etc/resolv.conf 中。可以配置多个。–dns-search=DOMAIN… 在该容器启动时,将 DOMAIN 添加到容器内 /etc/resolv.conf 的 dns search 列表中。可以配置多个。–dns-opt=OPTION… 在该容器启动时,将 OPTION 添加到容器内 /etc/resolv.conf 中的 options 选项中,可以配置多个。

说明:

如果 docker run 时不含 –dns=IP_ADDRESS…, –dns-search=DOMAIN…, or –dns-opt=OPTION… 参数,docker daemon 会将 copy 本主机的 /etc/resolv.conf,然后对该 copy 进行处理(将那些 /etc/resolv.conf 中 ping 不通的 nameserver 项给抛弃), 处理完成后留下的部分就作为该容器内部的 /etc/resolv.conf。因此,如果你想利用宿主机中的 /etc/resolv.conf 配置的 nameserver 进行域名解析,那么你需要宿主机中该 dns service 配置一个宿主机内容器能 ping 通的 IP。

如果宿主机的 /etc/resolv.conf 内容发生改变,docker daemon 有一个对应的 file change notifier 会 watch 到这一变化,然后根据容器状态采取对应的措施:

如果容器启动时,用了 –dns, –dns-search, or –dns-opt 选项,其启动时已经修改了宿主机的 /etc/resolv.conf 过滤后的内容,因此 docker daemon 永远不会更新这种容器的 /etc/resolv.conf。

如果容器状态为 stopped,则立刻根据宿主机的 /etc/resolv.conf 内容更新容器内的 /etc/resolv.conf.

如果容器状态为 running,则容器内的 /etc/resolv.conf 将不会改变,直到该容器状态变为 stopped.

如果容器启动后修改过容器内的 /etc/resolv.conf,则不会对该容器进行处理,否则可能会丢失已经完成的修改,无论该容器为什么状态。

注意: docker daemon 监控宿主机 /etc/resolv.conf 的这个 file change notifier 的实现是依赖 linux 内核的 inotify 特性,而 inotfy 特性不兼容 overlay fs,因此使用 overlay fs driver 的 docker deamon 将无法使用该 /etc/resolv.conf 自动更新的功能。、

Embedded DNS in user-defined networks

在 docker 1.10 版本中,docker daemon 实现了一个叫做 embedded DNS server 的东西,用来当你创建的容器满足以下条件时:

使用自定义网络;

容器创建时候通过 –name,–network-alias or –link 提供了一个 name;

docker daemon 就会利用 embedded DNS server 对整个自定义网络中所有容器进行名字解析(你可以理解为一个网络中的一种服务发现)。

因此当你启动容器时候满足以上条件时,该容器的域名解析就不应该去考虑容器内的 /etc/hosts, /etc/resolv.conf,应该保持其不变,甚至为空,将需要解析的域名都配置到对应 embedded DNS server 中。具体配置参数及说明如下:

OptionsDescription–name=CONTAINER-NAME 在该容器启动时,会将 CONTAINER-NAME 和该容器的 IP 配置到该容器连接到的自定义网络中的 embedded DNS server 中,由它提供该自定义网络范围内的域名解析 –network-alias=ALIAS 将容器的 name-ip map 配置到容器连接到的其他网络的 embedded DNS server 中。PS:一个容器可能连接到多个网络中。–link=CONTAINER_NAME:ALIAS 在该容器启动时,将 ALIAS 和 CONTAINER_NAME/ID 对应的容器 IP 配置到该容器连接到的自定义网络中的 embedded DNS server 中,但仅限于配置了该 link 的容器能解析这条 rule。–dns=[IP_ADDRESS…] 当 embedded DNS server 无法解析该容器的某个 dns query 时,会将请求 foward 到这些 –dns 配置的 IP_ADDRESS DNS Server,由它们进一步进行域名解析。注意,这些 –dns 配置到 nameserver IP_ADDRESS 全部由对应的 embedded DNS server 管理,并不会更新到容器内的 /etc/resolv.conf.–dns-search=DOMAIN… 在该容器启动时,会将 –dns-search 配置的 DOMAIN 们配置到 the embedded DNS server,并不会更新到容器内的 /etc/resolv.conf。–dns-opt=OPTION… 在该容器启动时,会将 –dns-opt 配置的 OPTION 们配置到 the embedded DNS server,并不会更新到容器内的 /etc/resolv.conf。

说明:

如果 docker run 时不含 –dns=IP_ADDRESS…, –dns-search=DOMAIN…, or –dns-opt=OPTION… 参数,docker daemon 会将 copy 本主机的 /etc/resolv.conf,然后对该 copy 进行处理(将那些 /etc/resolv.conf 中 ping 不通的 nameserver 项给抛弃), 处理完成后留下的部分就作为该容器内部的 /etc/resolv.conf。因此,如果你想利用宿主机中的 /etc/resolv.conf 配置的 nameserver 进行域名解析,那么你需要宿主机中该 dns service 配置一个宿主机内容器能 ping 通的 IP。

注意容器内 /etc/resolv.conf 中配置的 DNS server,只有当 the embedded DNS server 无法解析某个 name 时,才会用到。

embedded DNS server 源码分析

所有 embedded DNS server 相关的代码都在 libcontainer 项目中,几个最主要的文件分别是 /libnetwork/resolver.go,/libnetwork/resolver_unix.go,sandbox_dns_unix.go。

OK, 先来看看 embedded DNS server 对象在 docker 中的定义:

libnetwork/resolver.go
// resolver implements the Resolver interface
type resolver struct {
 sb *sandbox
 extDNSList [maxExtDNS]extDNSEntry
 server *dns.Server
 conn *net.UDPConn
 tcpServer *dns.Server
 tcpListen *net.TCPListener
 err error
 count int32
 tStamp time.Time
 queryLock sync.Mutex
// Resolver represents the embedded DNS server in Docker. It operates
// by listening on container s loopback interface for DNS queries.
type Resolver interface {
 // Start starts the name server for the container
 Start() error
 // Stop stops the name server for the container. Stopped resolver
 // can be reused after running the SetupFunc again.
 Stop()
 // SetupFunc() provides the setup function that should be run
 // in the container s network namespace.
 SetupFunc() func()
 // NameServer() returns the IP of the DNS resolver for the
 // containers.
 NameServer() string
 // SetExtServers configures the external nameservers the resolver
 // should use to forward queries
 SetExtServers([]string)
 // ResolverOptions returns resolv.conf options that should be set
 ResolverOptions() []string
}

可见,resolver 就是 embedded DNS server,每个 resolver 都 bind 一个 sandbox,并定义了一个对应的 dns.Server,还定义了外部 DNS 对象列表,但 embedded DNS server 无法解析某个 name 时,就会 forward 到那些外部 DNS。

Resolver Interface 定义了 embedded DNS server 必须实现的接口,这里会重点关注 SetupFunc() 和 Start(),见下文分析。

dns.Server 的实现,全部交给 github.com/miekg/dns,限于篇幅,这里我将不会跟进去分析。

从整个 container create 的流程上来看,docker daemon 对 embedded DNS server 的处理是从 endpoint Join a sandbox 开始的:

libnetwork/endpoint.go

func (ep *endpoint) Join(sbox Sandbox, options ...EndpointOption) error {return ep.sbJoin(sb, options...)
func (ep *endpoint) sbJoin(sb *sandbox, options ...EndpointOption) error {if err = sb.populateNetworkResources(ep); err != nil {return err}

sandbox join a sandbox 的流程中,会调用 sandbox. populateNetworkResources 做网络资源的设置,这其中就包括了 embedded DNS server 的启动。

libnetwork/sandbox.go
func (sb *sandbox) populateNetworkResources(ep *endpoint) error {if ep.needResolver() {sb.startResolver(false)

libnetwork/sandbox_dns_unix.go func (sb *sandbox) startResolver(restore bool) {sb.resolverOnce.Do(func() { var err error sb.resolver = NewResolver(sb) defer func() { if err != nil { sb.resolver = nil // In the case of live restore container is already running with // right resolv.conf contents created before. Just update the // external DNS servers from the restored sandbox for embedded // server to use. if !restore {err = sb.rebuildDNS() if err != nil {log.Errorf( Updating resolv.conf failed for container %s, %q , sb.ContainerID(), err) return sb.resolver.SetExtServers(sb.extDNS) sb.osSbox.InvokeFunc(sb.resolver.SetupFunc()) if err = sb.resolver.Start(); err != nil {log.Errorf( Resolver Setup/Start failed for container %s, %q , sb.ContainerID(), err) }

sandbox.startResolver 是流程关键:

通过 sanbdox.rebuildDNS 生成了 container 内的 /etc/resolv.conf

通过 resolver.SetExtServers(sb.extDNS) 设置 embedded DNS server 的 forward DNS list

通过 resolver.SetupFunc() 启动两个随机可用端口作为 embedded DNS server(127.0.0.11)的 TCP 和 UDP Linstener

通过 resolver.Start() 对容器内的 iptable 进行设置 ( 见下),并通过 miekg/dns 启动一个 nameserver 在 53 端口提供服务。

下面我将逐一介绍上面的各个步骤。

sanbdox.rebuildDNS

sanbdox.rebuildDNS 负责构建容器内的 resolv.conf,构建规则就是第一节江参数配置时候提到的:

Save the external name servers in resolv.conf in the sandbox

Add only the embedded server s IP to container s resolv.conf

If the embedded server needs any resolv.conf options add it to the current list

libnetwork/sandbox_dns_unix.go
func (sb *sandbox) rebuildDNS() error {currRC, err := resolvconf.GetSpecific(sb.config.resolvConfPath)
 if err != nil {
 return err
 // localhost entries have already been filtered out from the list
 // retain only the v4 servers in sb for forwarding the DNS queries
 sb.extDNS = resolvconf.GetNameservers(currRC.Content, types.IPv4)
 var (dnsList = []string{sb.resolver.NameServer()}
 dnsOptionsList = resolvconf.GetOptions(currRC.Content)
 dnsSearchList = resolvconf.GetSearchDomains(currRC.Content)
 dnsList = append(dnsList, resolvconf.GetNameservers(currRC.Content, types.IPv6)...)
 resOptions := sb.resolver.ResolverOptions()
dnsOpt:
 dnsOptionsList = append(dnsOptionsList, resOptions...)
 _, err = resolvconf.Build(sb.config.resolvConfPath, dnsList, dnsSearchList, dnsOptionsList)
 return err
}

resolver.SetExtServers

设置 embedded DNS server 的 forward DNS list, 当 embedded DNS server 不能解析某 name 时,就会将请求 forward 到 ExtServers。代码很简单,不多废话。

libnetwork/resolver.go
func (r *resolver) SetExtServers(dns []string) {l := len(dns)
 if l   maxExtDNS {
 l = maxExtDNS
 for i := 0; i   l; i++ {r.extDNSList[i].ipStr = dns[i]
}

resolver.SetupFunc

启动两个随机可用端口作为 embedded DNS server(127.0.0.11)的 TCP 和 UDP Linstener。

libnetwork/resolver.go
func (r *resolver) SetupFunc() func() {return (func() {
 var err error
 // DNS operates primarily on UDP
 addr :=  net.UDPAddr{IP: net.ParseIP(resolverIP),
 r.conn, err = net.ListenUDP(udp , addr)
 // Listen on a TCP as well
 tcpaddr :=  net.TCPAddr{IP: net.ParseIP(resolverIP),
 r.tcpListen, err = net.ListenTCP(tcp , tcpaddr)
}

resolver.Start

resolver.Start 中两个重要步骤,分别是:

setupIPTable 设置容器内的 iptables

启动 dns nameserver 在 53 端口开始提供域名解析服务

func (r *resolver) Start() error {if err := r.setupIPTable(); err != nil {return fmt.Errorf( setting up IP table rules failed: %v , err)
 tcpServer :=  dns.Server{Handler: r, Listener: r.tcpListen}
 r.tcpServer = tcpServer
 go func() {tcpServer.ActivateAndServe()
 return nil
}

先来看看怎么设置容器内的 iptables 的:

func (r *resolver) setupIPTable() error {//  获取 setupFunc() 时的两个本地随机监听端口
 laddr := r.conn.LocalAddr().String()
 ltcpaddr := r.tcpListen.Addr().String()
 cmd :=  exec.Cmd{Path: reexec.Self(),
 //  将这两个端口传给 setup-resolver 命令并启动执行
 Args: append([]string{ setup-resolver}, r.sb.Key(), laddr, ltcpaddr),
 Stdout: os.Stdout,
 Stderr: os.Stderr,
 if err := cmd.Run(); err != nil {return fmt.Errorf( reexec failed: %v , err)
 return nil
// init 时就注册 setup-resolver 对应的 handler
func init() {reexec.Register( setup-resolver , reexecSetupResolver)
// setup-resolver 对应的 handler 定义
func reexecSetupResolver() {
 //  封装 iptables 数据
 _, ipPort, _ := net.SplitHostPort(os.Args[2])
 _, tcpPort, _ := net.SplitHostPort(os.Args[3])
 rules := [][]string{{ -t ,  nat ,  -I , outputChain,  -d , resolverIP,  -p ,  udp ,  --dport , dnsPort,  -j ,  DNAT ,  --to-destination , os.Args[2]},
 {-t ,  nat ,  -I , postroutingchain,  -s , resolverIP,  -p ,  udp ,  --sport , ipPort,  -j ,  SNAT ,  --to-source ,  :  + dnsPort},
 {-t ,  nat ,  -I , outputChain,  -d , resolverIP,  -p ,  tcp ,  --dport , dnsPort,  -j ,  DNAT ,  --to-destination , os.Args[3]},
 {-t ,  nat ,  -I , postroutingchain,  -s , resolverIP,  -p ,  tcp ,  --sport , tcpPort,  -j ,  SNAT ,  --to-source ,  :  + dnsPort},
 // insert outputChain and postroutingchain
}

在 reexecSetupResolver() 中清楚的定义了 iptables 添加 outputChain 和 postroutingchain,将到容器内的 dns query 请求重定向到 embedded DNS server(127.0.0.11) 上的 udp/tcp 两个随机可用端口,embedded DNS server(127.0.0.11) 的返回数据则重定向到容器内的 53 端口,这样完成了整个 dns query 请求。

模型图如下:

贴一张实例图:

到这里,关于 embedded DNS server 的源码分析就结束了。当然,其中还有很多细节,就留给读者自己走读代码了。

福利

另外,借用同事 wuke 之前画的一个时序图,看看 embedded DNS server 的操作在整个容器 create 流程中的位置,我就不重复造轮子了。

以上是“docker container DNS 如何配置”这篇文章的所有内容,感谢各位的阅读!相信大家都有了一定的了解,希望分享的内容对大家有所帮助,如果还想学习更多知识,欢迎关注丸趣 TV 行业资讯频道!

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-08-16发表,共计9822字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)