docker container DNS配置介紹和原始碼分析
本文主要介紹了docker容器的DNS配置及其注意點,重點對docker 1.10釋出的embedded DNS server進行了原始碼分析,看看embedded DNS server到底是個啥,它是如何工作的。
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到這一變化,然後根據容器狀態採取對應的措施:
- 如果容器狀態為stopped,則立刻根據宿主機的/etc/resolv.conf內容更新容器內的/etc/resolv.conf.
- 如果容器狀態為running,則容器內的/etc/resolv.conf將不會改變,直到該容器狀態變為stopped.
- 如果容器啟動後修改過容器內的/etc/resolv.conf,則不會對該容器進行處理,否則可能會丟失已經完成的修改,無論該容器為什麼狀態。
- 如果容器啟動時,用了–dns, –dns-search, or –dns-opt選項,其啟動時已經修改了宿主機的/etc/resolv.conf過濾後的內容,因此docker daemon永遠不會更新這種容器的/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請求。
模型圖如下:
貼一張例項圖:
到這裡,關於embedded DNS server的原始碼分析就結束了。當然,其中還有很多細節,就留給讀者自己走讀程式碼了。
福利
另外,借用同事wuke之前畫的一個時序圖,看看embedded DNS server的操作在整個容器create流程中的位置,我就不重複造輪子了。