docker container DNS如何配置

Configure container DNS

DNS in default bridge network

Options Description
-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中。具体配置参数及说明如下:

Options Description
–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请求。

模型图如下: docker container DNS如何配置

贴一张实例图: docker container DNS如何配置

docker container DNS如何配置

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

福利

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

原创文章,作者:SUFNF,如若转载,请注明出处:http://www.wangzhanshi.com/n/12950.html

(0)
SUFNF的头像SUFNF
上一篇 2025年1月1日 17:04:10
下一篇 2025年1月1日 17:04:12

相关推荐

  • Linux怎么配置DNS域名解析

    Linux下配置dns的三种方法    \1. HOST 本地DNS解析 vi /etc/hosts  eg:23.231.234.33 www.ba…

    2025年1月1日
  • win7怎么修复dns异常

    win7修复dns异常 1、打开控制面板,选择查看网络状态和任务 2、选择更改适配器。 3、右键我们连接的网络的图标,点击属性。 4、点击tcp/ipv4协议,选择”使用下面的dn…

    2025年1月1日
  • DNS缓存中毒是怎么回事?

    近来,网络上出现互联网漏洞——DNS缓存漏洞,此漏洞直指我们应用中互联网脆弱的安全系统,而安全性差的根源在于设计缺陷。利用该漏洞轻则可以让用户无法打开网页,重则是网络钓鱼和金融诈骗…

    2025年1月1日
  • windows谷歌浏览器dns未响应如何解决

    解决方法: 1、依次进入“电脑—>网络—>网络和共享中心—>更改适配器设置—>找到你连接的网络”, 右击“属性—>Internet协议版本4”查看是自…

    2025年1月1日
  • DNS协议知识点有哪些

    DNS 服务器 每个人上网,都会访问网址,都会用到 DNS ,所以它是非常重要的,但这也是对它的挑战。因为只要它一旦出现了故障,那么整个互联网都几乎会瘫痪,因为你访问不到网址了嘛。…

    DNS解析 2025年1月1日
  • dns异常如何修复

    dns异常的修复方法:1、在搜索框中搜索控制面板,点击打开;2、进入网络和共享中心选项 ,点击更改适配器设置;3、打开属性,手动输入公共dns地址,点击【确定】即可。 具体方法: …

    DNS解析 2025年1月1日
  • 如何进行RHEL5 DNS配置

    在RHEL4的基础上添加了RHEL5,同样系统的完善,也让跟多的人应用了RHEL5,所以这就要求RHEL5在配置上要更加强大,在服务配置上主要是RHEL5 DNS配置,所以下面就来…

    DNS解析 2025年1月1日
  • 如何在DreamHost共享主机上新增站点与设定GoDaddy DNS

    在 DreamHost 网页主机空间新增网站与 FTP 账号在购买了 DreamHost 的网页主机空间之后,接下来就是要开始新增网站,正常来说在购买的过程中我们所填入的网站在这时…

    2025年1月1日
  • DNS检测的特征以及BotDAD安装与使用

    一、基于DNS的隐蔽通信 企业网络经常面临网络攻击者窃取有价值和敏感数据的威胁。复杂的攻击者越来越多地利用DNS通道来泄露数据,以及维护恶意软件的隧道C&C(命令和控制)通…

    2025年1月1日
  • 怎么分析vlan、三层交换机、网关、DNS、子网掩码和MAC地址

    一、什么是VLAN VLAN中文是“虚拟局域网”。LAN可以是由少数几台家用计算机构成的网络,也可以是数以百计的计算机构成的企业网络。VLAN所指的LAN特指使用路由器分割的网络—…

    2025年1月1日

发表回复

登录后才能评论