本文主要介紹了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中的定義: ```golang 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開始的: ```golang 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流程中的位置,我就不重復造輪子了。 
Tags: default network search IP地址 server
文章來源: