coredns源码分析

coredns源码分析k8scoredns 源码

CoreDNS是使用go语言编写的快速灵活的DNS服务,采用链式插件模式,每个插件实现独立的功能,底层协议可以是tcp/udp,也可以是TLS,gRPC等。默认监听所有ip地址,可使用bind插件指定监听指定地址。

配置文件

格式如下

[SCHEME://]ZONE [[SCHEME://]ZONE]...[:PORT] { [PLUGIN]... } 

一块上面格式的配置表示一个dnsserver,称为serverblock,可以配置多个serverblock表示多个dnsserver。

下面通过一个例子说明,如下配置文件指定了4个serverblock,即4个dnsserver,第一个监听端口5300,后面三个监听同一个端口53,每个dnsserver指定了特定的插件。

coredns.io:5300 { file /etc/coredns/zones/coredns.io.db } example.io:53 { log errors file /etc/coredns/zones/example.io.db } example.net:53 { file /etc/coredns/zones/example.net.db } .:53 { kubernetes errors log health } 

下图为配置的简略图

coredns源码分析

image.png

Handler interface { ServeDNS(context.Context, dns.ResponseWriter, *dns.Msg) (int, error) Name() string } 

dns请求处理流程
收到dns请求后,首先根据域名匹配zone找到对应的dnsserver(最长匹配优先),如果没有匹配到,则使用默认的root dnsserver。
找到dnsserver后,就要按照插件顺序执行其中配置的插件,当然并不是配置的插件都会被执行,如果某个插件成功找到记录,则返回成功,否则根据插件是否配置了fallthrough等来决定是否执行下一个插件。




源码分析

plugin.cfg
源码目录下的plugin.cfg指定了插件执行顺序,如果想添加插件,可按格式添加到指定位置。

metadata:metadata geoip:geoip cancel:cancel tls:tls reload:reload nsid:nsid bufsize:bufsize root:root bind:bind debug:debug trace:trace ready:ready health:health pprof:pprof prometheus:metrics errors:errors log:log dnstap:dnstap local:local dns64:dns64 acl:acl any:any chaos:chaos loadbalance:loadbalance cache:cache rewrite:rewrite header:header dnssec:dnssec autopath:autopath minimal:minimal template:template transfer:transfer hosts:hosts route53:route53 azure:azure clouddns:clouddns k8s_external:k8s_external kubernetes:kubernetes file:file auto:auto secondary:secondary etcd:etcd loop:loop forward:forward grpc:grpc erratic:erratic whoami:whoami on:github.com/coredns/caddy/onevent sign:sign 

源码目录下的Makefile根据plugin.cfg生成了两个go文件:zplugin.go和zdirectives.go。

core/plugin/zplugin.go core/dnsserver/zdirectives.go: plugin.cfg go generate coredns.go go get 

core/plugin/zplugin.go会导入所有的插件,执行所有插件的init函数。 import ( // Include all plugins. _ "github.com/coredns/caddy/onevent" _ "github.com/coredns/coredns/plugin/acl" _ "github.com/coredns/coredns/plugin/any" _ "github.com/coredns/coredns/plugin/auto" _ "github.com/coredns/coredns/plugin/autopath" _ "github.com/coredns/coredns/plugin/azure" _ "github.com/coredns/coredns/plugin/bind" _ "github.com/coredns/coredns/plugin/bufsize" _ "github.com/coredns/coredns/plugin/cache" _ "github.com/coredns/coredns/plugin/cancel" _ "github.com/coredns/coredns/plugin/chaos" _ "github.com/coredns/coredns/plugin/clouddns" _ "github.com/coredns/coredns/plugin/debug" _ "github.com/coredns/coredns/plugin/dns64" _ "github.com/coredns/coredns/plugin/dnssec" _ "github.com/coredns/coredns/plugin/dnstap" _ "github.com/coredns/coredns/plugin/erratic" _ "github.com/coredns/coredns/plugin/errors" _ "github.com/coredns/coredns/plugin/etcd" _ "github.com/coredns/coredns/plugin/file" _ "github.com/coredns/coredns/plugin/forward" _ "github.com/coredns/coredns/plugin/geoip" _ "github.com/coredns/coredns/plugin/grpc" _ "github.com/coredns/coredns/plugin/header" _ "github.com/coredns/coredns/plugin/health" _ "github.com/coredns/coredns/plugin/hosts" _ "github.com/coredns/coredns/plugin/k8s_external" _ "github.com/coredns/coredns/plugin/kubernetes" _ "github.com/coredns/coredns/plugin/loadbalance" _ "github.com/coredns/coredns/plugin/local" _ "github.com/coredns/coredns/plugin/log" _ "github.com/coredns/coredns/plugin/loop" _ "github.com/coredns/coredns/plugin/metadata" _ "github.com/coredns/coredns/plugin/metrics" _ "github.com/coredns/coredns/plugin/minimal" _ "github.com/coredns/coredns/plugin/nsid" _ "github.com/coredns/coredns/plugin/pprof" _ "github.com/coredns/coredns/plugin/ready" _ "github.com/coredns/coredns/plugin/reload" _ "github.com/coredns/coredns/plugin/rewrite" _ "github.com/coredns/coredns/plugin/root" _ "github.com/coredns/coredns/plugin/route53" _ "github.com/coredns/coredns/plugin/secondary" _ "github.com/coredns/coredns/plugin/sign" _ "github.com/coredns/coredns/plugin/template" _ "github.com/coredns/coredns/plugin/tls" _ "github.com/coredns/coredns/plugin/trace" _ "github.com/coredns/coredns/plugin/transfer" _ "github.com/coredns/coredns/plugin/whoami" ) 

core/dnsserver/zdirectives.go将所有插件名字放在一个数组中。

var Directives = []string{ "metadata", "geoip", "cancel", "tls", "reload", "nsid", "bufsize", "root", "bind", "debug", "trace", "ready", "health", "pprof", "prometheus", "errors", "log", "dnstap", "local", "dns64", "acl", "any", "chaos", "loadbalance", "cache", "rewrite", "header", "dnssec", "autopath", "minimal", "template", "transfer", "hosts", "route53", "azure", "clouddns", "k8s_external", "kubernetes", "file", "auto", "secondary", "etcd", "loop", "forward", "grpc", "erratic", "whoami", "on", "sign", } 

codedns 主函数

//coredns/coredns.go import ( _ "github.com/coredns/coredns/core/plugin" // Plug in CoreDNS. "github.com/coredns/coredns/coremain" ) main coremain.Run() 

codedns.go 首先导入了包”github.com/coredns/coredns/core/plugin”,此包内只有一个文件zplugin.go,此文件为自动生成的,主要导入了所有的插件,执行每个插件的init函数。

接着执行 run.go Run

//coredns/coremain/run.go import ( ... "github.com/coredns/coredns/core/dnsserver" ) func Run() //解析参数 flag.Parse() //如果指定了参数 version,则打印版本信息后退出 if version { showVersion() os.Exit(0) } //如果指定了参数 plugins,则只打印插件信息后退出 if plugins { fmt.Println(caddy.DescribePlugins()) os.Exit(0) } //解析配置文件 corefile, err := caddy.LoadCaddyfile(serverType) cdyfile, err := loadCaddyfileInput(serverType) for _, l := range caddyfileLoaders { //执行 confLoader cdyfile, err := l.loader.Load(serverType) } instance, err := caddy.Start(corefile) // Twiddle your thumbs instance.Wait() 

此文件又引入了包”github.com/coredns/coredns/core/dnsserver”,其init函数在 dnsserver/register.go 文件中,如下所示,主要是注册了serverType

const serverType = "dns" // DefaultPort is the default port. const DefaultPort = transport.Port Port = "53" func init() { flag.StringVar(&Port, serverType+".port", DefaultPort, "Default port") flag.StringVar(&Port, "p", DefaultPort, "Default port") caddy.RegisterServerType(serverType, caddy.ServerType{ Directives: func() []string { return Directives }, DefaultInput: func() caddy.Input { return caddy.CaddyfileInput{ Filepath: "Corefile", Contents: []byte(".:" + Port + " {\nwhoami\nlog\n}\n"), ServerTypeName: serverType, } }, NewContext: newContext, }) } 

//caddy/caddy.go func Start(cdyfile Input) (*Instance, error) inst := &Instance{serverType: cdyfile.ServerType(), wg: new(sync.WaitGroup), Storage: make(map[interface{}]interface{})} err := startWithListenerFds(cdyfile, inst, nil) func startWithListenerFds(cdyfile Input, inst *Instance, restartFds map[string]restartTriple) error { ValidateAndExecuteDirectives(cdyfile, inst, false) //stypeName 为 dns stypeName := cdyfile.ServerType() //stype 通过 RegisterServerType 注册,在 //coredns/core/dnsserver/register.go init时注册 stype, err := getServerType(stypeName) stype, ok := serverTypes[serverType] if ok { return stype, nil } ... inst.caddyfileInput = cdyfile //func loadServerBlocks(serverType, filename string, input io.Reader) ([]caddyfile.ServerBlock, error) sblocks, err := loadServerBlocks(stypeName, cdyfile.Path(), bytes.NewReader(cdyfile.Body())) validDirectives := ValidDirectives(serverType) serverBlocks, err := caddyfile.Parse(filename, input, validDirectives) p := parser{Dispenser: NewDispenser(filename, input), validDirectives: validDirectives} // NewDispenser returns a Dispenser, ready to use for parsing the given input. func NewDispenser(filename string, input io.Reader) Dispenser { tokens, _ := allTokens(input) // ignoring error because nothing to do with it return Dispenser{ filename: filename, tokens: tokens, cursor: -1, } } return p.parseAll() //coredns/core/dnsserver/register.go:newContext inst.context = stype.NewContext(inst) //coredns/core/dnsserver/register.go:InspectServerBlocks sblocks, err = inst.context.InspectServerBlocks(cdyfile.Path(), sblocks) return executeDirectives(inst, cdyfile.Path(), stype.Directives(), sblocks, justValidate) //遍历执行插件注册的 setup 函数 for _, dir := range directives { for i, sb := range sblocks { //获取插件的初始化函数 setup setup, err := DirectiveAction(inst.serverType, dir) if stypePlugins, ok := plugins[serverType]; ok { if plugin, ok := stypePlugins[dir]; ok { return plugin.Action, nil } } //执行插件注册的 setup 函数 setup(controller) //将 onStart 添加到数组 c.instance.OnStartup c.OnStartup(onStart) //每个插件的setup函数都会调用如下函数,注册插件handler //AddPlugin -> c.Plugin = append(c.Plugin, m) dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { l.Next = next return l }) } } slist, err := inst.context.MakeServers() errValid := h.validateZonesAndListeningAddresses() for _, c := range h.configs { c.Plugin = c.firstConfigInBlock.Plugin c.ListenHosts = c.firstConfigInBlock.ListenHosts c.Debug = c.firstConfigInBlock.Debug c.TLSConfig = c.firstConfigInBlock.TLSConfig } //将监听相同地址的config放在同一个group。一个config表示一个 dnsserver // we must map (group) each config to a bind address groups, err := groupConfigsByListenAddr(h.configs) groups := make(map[string][]*Config) for _, conf := range configs { for _, h := range conf.ListenHosts { addr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort(h, conf.Port)) if err != nil { return nil, err } addrstr := conf.Transport + "://" + addr.String() groups[addrstr] = append(groups[addrstr], conf) } } return groups, nil //为同一个组的config创建一个server,多个插件共享一个底层server,上层通过 //zone区分请求是到哪个dnsserver的 // then we create a server for each group var servers []caddy.Server for addr, group := range groups { // switch on addr switch tr, _ := parse.Transport(addr); tr { case transport.DNS: s, err := NewServer(addr, group) s := &Server{ Addr: addr, zones: make(map[string]*Config), graceTimeout: 5 * time.Second, } //site的类型是 Config,每个site表示一个dnsserver for _, site := range group { // set the config per zone s.zones[site.Zone] = site //遍历每个dnsserver配置的插件 //site.Plugin 为每个插件初始化setup时调用 dnsserver.GetConfig(c).AddPlugin 生成, //顺序是按照数组 Directives 从前向后 // compile custom plugin for everything var stack plugin.Handler //从后向前逆序遍历 site.Plugin for i := len(site.Plugin) - 1; i >= 0; i-- { stack = site.Plugin[i](stack) // register the *handler* also site.registerHandler(stack) c.registry[h.Name()] = h if s.trace == nil && stack.Name() == "trace" { // we have to stash away the plugin, not the // Tracer object, because the Tracer won't be initialized yet if t, ok := stack.(trace.Trace); ok { s.trace = t } } // Unblock CH class queries when any of these plugins are loaded. if _, ok := EnableChaos[stack.Name()]; ok { s.classChaos = true } } //pluginChain 为第一个插件的 handler,收到dns请求后,先执行第一个插件的 handler //在 ServeDNS(core/dnsserver/server.go) 函数中执行 pluginChain site.pluginChain = stack } return s, nil servers = append(servers, s) ... } } return servers, nil for _, startupFunc := range inst.OnStartup { //比如 kubernetes 的 onStart 函数 err = startupFunc() } startServers(slist, inst, restartFds) for _, s := range serverList { if ln == nil { ln, err = s.Listen() //core/dnsserver/server.go:Listen l, err := reuseport.Listen("tcp", s.Addr[len(transport.DNS+"://"):]) return l, nil } if pc == nil { pc, err = s.ListenPacket() //core/dnsserver/server.go:ListenPacket p, err := reuseport.ListenPacket("udp", s.Addr[len(transport.DNS+"://"):]) return p, nil } inst.servers = append(inst.servers, ServerListener{server: s, listener: ln, packet: pc}) } for _, s := range inst.servers { func(s Server, ln net.Listener, pc net.PacketConn, inst *Instance) { go func() { defer func() { inst.wg.Done() stopWg.Done() }() errChan <- s.Serve(ln) }() go func() { defer func() { inst.wg.Done() stopWg.Done() }() errChan <- s.ServePacket(pc) }() }(s.server, s.listener, s.packet, inst) } } 

tcp协议调用Serve,udp协议调用ServePacket

//core/dnsserver/server.go // Serve starts the server with an existing listener. It blocks until the server stops. // This implements caddy.TCPServer interface. func (s *Server) Serve(l net.Listener) error { s.m.Lock() s.server[tcp] = &dns.Server{Listener: l, Net: "tcp", Handler: dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) { ctx := context.WithValue(context.Background(), Key{}, s) ctx = context.WithValue(ctx, LoopKey{}, 0) s.ServeDNS(ctx, w, r) })} s.m.Unlock() return s.server[tcp].ActivateAndServe() } // ServePacket starts the server with an existing packetconn. It blocks until the server stops. // This implements caddy.UDPServer interface. func (s *Server) ServePacket(p net.PacketConn) error { s.m.Lock() s.server[udp] = &dns.Server{PacketConn: p, Net: "udp", Handler: dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) { ctx := context.WithValue(context.Background(), Key{}, s) ctx = context.WithValue(ctx, LoopKey{}, 0) s.ServeDNS(ctx, w, r) })} s.m.Unlock() return s.server[udp].ActivateAndServe() } 

收到DNS请求后,调用ServeDNS,根据域名匹配dnsserver,如果没有匹配不到则使用根dnsserver,然后执行dnsserver中配置的插件

// ServeDNS is the entry point for every request to the address that // is bound to. It acts as a multiplexer for the requests zonename as // defined in the request so that the correct zone // (configuration and plugin stack) will handle the request. func (s *Server) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) { // The default dns.Mux checks the question section size, but we have our // own mux here. Check if we have a question section. If not drop them here. if r == nil || len(r.Question) == 0 { errorAndMetricsFunc(s.Addr, w, r, dns.RcodeServerFailure) return } // Wrap the response writer in a ScrubWriter so we automatically make the reply fit in the client's buffer. w = request.NewScrubWriter(r, w) q := strings.ToLower(r.Question[0].Name) var ( off int end bool dshandler *Config ) //根据dns请求的域名作为zone(最长匹配优先),遍历 s.zones 进行匹配(每个zone表示一个dnsserver), //如果匹配到了,则设置 dshandler = h for { if h, ok := s.zones[q[off:]]; ok { if h.pluginChain == nil { // zone defined, but has not got any plugins errorAndMetricsFunc(s.Addr, w, r, dns.RcodeRefused) return } if r.Question[0].Qtype != dns.TypeDS { rcode, _ := h.pluginChain.ServeDNS(ctx, w, r) if !plugin.ClientWrite(rcode) { errorFunc(s.Addr, w, r, rcode) } return } // The type is DS, keep the handler, but keep on searching as maybe we are serving // the parent as well and the DS should be routed to it - this will probably *misroute* DS // queries to a possibly grand parent, but there is no way for us to know at this point // if there is an actual delegation from grandparent -> parent -> zone. // In all fairness: direct DS queries should not be needed. dshandler = h } off, end = dns.NextLabel(q, off) if end { break } } //匹配到zone,执行dnsserver的插件的 ServeDNS。 //如果插件的 ServeDNS 直接返回了(比如k8s插件查找成功时),则只执行一个插件, //如果插件的 ServeDNS 调用plugin.NextOrFailure,则开始执行下一个插件 ServeDNS 了, //依次类推,直到有插件返回成功或者失败。 if r.Question[0].Qtype == dns.TypeDS && dshandler != nil && dshandler.pluginChain != nil { // DS request, and we found a zone, use the handler for the query. rcode, _ := dshandler.pluginChain.ServeDNS(ctx, w, r) if !plugin.ClientWrite(rcode) { errorFunc(s.Addr, w, r, rcode) } return } //如果dnsserver没有匹配的zone,则最后尝试执行根zone,即配置文件中指定的"." // Wildcard match, if we have found nothing try the root zone as a last resort. if h, ok := s.zones["."]; ok && h.pluginChain != nil { rcode, _ := h.pluginChain.ServeDNS(ctx, w, r) if !plugin.ClientWrite(rcode) { errorFunc(s.Addr, w, r, rcode) } return } // Still here? Error out with REFUSED. errorAndMetricsFunc(s.Addr, w, r, dns.RcodeRefused) } 

以k8s插件为例

//k8s插件的 ServeDNS 函数 // ServeDNS implements the plugin.Handler interface. func (k Kubernetes) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { state := request.Request{W: w, Req: r} qname := state.QName() zone := plugin.Zones(k.Zones).Matches(qname) if zone == "" { return plugin.NextOrFailure(k.Name(), k.Next, ctx, w, r) } zone = qname[len(qname)-len(zone):] // maintain case of original query state.Zone = zone var ( records []dns.RR extra []dns.RR truncated bool err error ) switch state.QType() { case dns.TypeA: records, truncated, err = plugin.A(ctx, &k, zone, state, nil, plugin.Options{}) case dns.TypeAAAA: records, truncated, err = plugin.AAAA(ctx, &k, zone, state, nil, plugin.Options{}) case dns.TypeTXT: records, truncated, err = plugin.TXT(ctx, &k, zone, state, nil, plugin.Options{}) case dns.TypeCNAME: records, err = plugin.CNAME(ctx, &k, zone, state, plugin.Options{}) case dns.TypePTR: records, err = plugin.PTR(ctx, &k, zone, state, plugin.Options{}) case dns.TypeMX: records, extra, err = plugin.MX(ctx, &k, zone, state, plugin.Options{}) case dns.TypeSRV: records, extra, err = plugin.SRV(ctx, &k, zone, state, plugin.Options{}) case dns.TypeSOA: if qname == zone { records, err = plugin.SOA(ctx, &k, zone, state, plugin.Options{}) } case dns.TypeAXFR, dns.TypeIXFR: return dns.RcodeRefused, nil case dns.TypeNS: if state.Name() == zone { records, extra, err = plugin.NS(ctx, &k, zone, state, plugin.Options{}) break } fallthrough default: // Do a fake A lookup, so we can distinguish between NODATA and NXDOMAIN fake := state.NewWithQuestion(state.QName(), dns.TypeA) fake.Zone = state.Zone _, _, err = plugin.A(ctx, &k, zone, fake, nil, plugin.Options{}) } //没有查找到 dns 记录时,如果配置了fallthrough,则执行下一个插件, //否则返回错误信息 if k.IsNameError(err) { if k.Fall.Through(state.Name()) { return plugin.NextOrFailure(k.Name(), k.Next, ctx, w, r) } if !k.APIConn.HasSynced() { // If we haven't synchronized with the kubernetes cluster, return server failure return plugin.BackendError(ctx, &k, zone, dns.RcodeServerFailure, state, nil /* err */, plugin.Options{}) } return plugin.BackendError(ctx, &k, zone, dns.RcodeNameError, state, nil /* err */, plugin.Options{}) } if err != nil { return dns.RcodeServerFailure, err } if len(records) == 0 { return plugin.BackendError(ctx, &k, zone, dns.RcodeSuccess, state, nil, plugin.Options{}) } //查到dns记录,返回dns响应 m := new(dns.Msg) m.SetReply(r) m.Truncated = truncated m.Authoritative = true m.Answer = append(m.Answer, records...) m.Extra = append(m.Extra, extra...) w.WriteMsg(m) return dns.RcodeSuccess, nil } // SRV returns SRV records from the Backend. // If the Target is not a name but an IP address, a name is created on the fly. func SRV(ctx context.Context, b ServiceBackend, zone string, state request.Request, opt Options) (records, extra []dns.RR, err error) { //比如对于 kubernetes 插件来说,b.Services 为 coredns/plugin/kubernetes/kubernetes.go:Services services, err := b.Services(ctx, state, false, opt) dup := make(map[item]struct{}) lookup := make(map[string]struct{}) // Looping twice to get the right weight vs priority. This might break because we may drop duplicate SRV records latter on. w := make(map[int]int) for _, serv := range services { weight := 100 if serv.Weight != 0 { weight = serv.Weight } if _, ok := w[serv.Priority]; !ok { w[serv.Priority] = weight continue } w[serv.Priority] += weight } for _, serv := range services { // Don't add the entry if the port is -1 (invalid). The kubernetes plugin uses port -1 when a service/endpoint // does not have any declared ports. if serv.Port == -1 { continue } w1 := 100.0 / float64(w[serv.Priority]) if serv.Weight == 0 { w1 *= 100 } else { w1 *= float64(serv.Weight) } weight := uint16(math.Floor(w1)) // weight should be at least 1 if weight == 0 { weight = 1 } what, ip := serv.HostType() switch what { case dns.TypeCNAME: srv := serv.NewSRV(state.QName(), weight) records = append(records, srv) if _, ok := lookup[srv.Target]; ok { break } lookup[srv.Target] = struct{}{} if !dns.IsSubDomain(zone, srv.Target) { m1, e1 := b.Lookup(ctx, state, srv.Target, dns.TypeA) if e1 == nil { extra = append(extra, m1.Answer...) } m1, e1 = b.Lookup(ctx, state, srv.Target, dns.TypeAAAA) if e1 == nil { // If we have seen CNAME's we *assume* that they are already added. for _, a := range m1.Answer { if _, ok := a.(*dns.CNAME); !ok { extra = append(extra, a) } } } break } // Internal name, we should have some info on them, either v4 or v6 // Clients expect a complete answer, because we are a recursor in their view. state1 := state.NewWithQuestion(srv.Target, dns.TypeA) addr, _, e1 := A(ctx, b, zone, state1, nil, opt) if e1 == nil { extra = append(extra, addr...) } // TODO(miek): AAAA as well here. case dns.TypeA, dns.TypeAAAA: addr := serv.Host serv.Host = msg.Domain(serv.Key) srv := serv.NewSRV(state.QName(), weight) if ok := isDuplicate(dup, srv.Target, "", srv.Port); !ok { records = append(records, srv) } if ok := isDuplicate(dup, srv.Target, addr, 0); !ok { extra = append(extra, newAddress(serv, srv.Target, ip, what)) } } } return records, extra, nil } func (k *Kubernetes) Services(ctx context.Context, state request.Request, exact bool, opt plugin.Options) (svcs []msg.Service, err error) { s, e := k.Records(ctx, state, false) r, e := parseRequest(state.Name(), state.Zone) services, err := k.findServices(r, state.Zone) //根据dns请求的service和namespace获取index idx := object.ServiceKey(r.service, r.namespace) //根据index从缓存获取 service 信息 serviceList = k.APIConn.SvcIndex(idx) endpointsListFunc = func() []*object.Endpoints { return k.APIConn.EpIndex(idx) } zonePath := msg.Path(zone, coredns) for _, svc := range serviceList { } return services, err 

参考
//如何写coredns插件
http://dockone.io/article/9620




 

也可参考:coredns源码分析 - 简书 (jianshu.com)

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/212331.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月18日 下午8:17
下一篇 2026年3月18日 下午8:18


相关推荐

  • 小程序的图片上传wx.uploadFile及后台PHP接收文件并存储到服务器[通俗易懂]

    小程序的图片上传wx.uploadFile及后台PHP接收文件并存储到服务器[通俗易懂]前台代码wxml:<buttonbindtap=’chooseImg’>选择图片</button>//图片选择<view><imagesrc='{{img_l}}’bindtap=’preview_img’></image></view>//图片预览<buttonbindtap=’up_img’…

    2022年6月17日
    50
  • 三角形中重心、内心、外心、垂心向量计算公式

    三角形中重心、内心、外心、垂心向量计算公式一 对 ABC 重心 O 来讲有 OA OB OC 0 mathop OA limits rightharpoon mathop OB limits rightharpoon mathop OC limits rightharpoon 0OA OB OC 0 证明 延长 CO 与线段 AB overline AB AB 交于点 D 根据 A D B 三点共线公式 OD mOA nOB mathop OD limits rightharpoon m

    2026年3月18日
    1
  • 10分钟彻底理解Redis持久化和主从复制「建议收藏」

    10分钟彻底理解Redis持久化和主从复制

    2022年2月9日
    47
  • 基于Neo4j构建的外贸企业关系图谱做企业相似度查询「建议收藏」

    基于Neo4j构建的外贸企业关系图谱做企业相似度查询「建议收藏」基于Neo4j的外贸企业关系图谱做企业相似度查询一、外贸企业关系图谱的构建1.从Oracle导出数据2.导入数据到Neo4j3.Neo4j数据展示二、用Cypher做企业关联查询1.多层关系查询2.基于邻居信息的Jaccard相似度计算3.加权关联度得分计算三、总结一、外贸企业关系图谱的构建说来惭愧,本科、研究生期间还没写过博客,正巧最近在写论文,想结合自己开发的项目来构思,于是就通过这篇博客记录一下使用Neo4j图数据库来做企业相似度查询的过程,方便以后参考。这次外贸企业关系图谱的构建用到以前项目中

    2022年6月26日
    29
  • 红罐王老吉品牌定位战略制定过程详解

    红罐王老吉品牌定位战略制定过程详解品牌释名凉茶是广东 广西地区的一种由中草药熬制 具有清热去湿等功效的 药茶 在众多老字号凉茶中 又以王老吉最为著名 王老吉凉茶发明于清道光年间 至今已有 175 年 被公认为凉茶始祖 有 药茶王 之称 到了近代 王老吉凉茶更随着华人的足迹遍及世界各地 20 世纪 50 年代初由于政治原因 王老吉凉茶铺分成两支 一支完成公有化改造 发展为今天的王老吉药业股份有限公司 生产王老吉凉茶颗粒

    2026年3月26日
    2
  • Emgucv环境配置[通俗易懂]

        Emgucv是在.NET平台下使用OpenCV视觉库的桥梁,在使用之前需要对系统进行配置,其配置和OpenCV的配置有点不同。1、EmguCV下载下载网站:http://www.emgu.com/wiki/index.php/Main_Page该网站上有EmguCV的所有资料,包括教程。下载好之后,直接安装到电脑上,安装位置可任意。本文所配置的是EmguCV3.0.0版本。2、新建一个VS…

    2022年4月14日
    89

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号