Q先生的世界

面朝大海,春暖花开

经典项目源码分析|CoreDNS 代码结构总览:入口、插件链、请求流与 dnsserver 骨架

很多人第一次接触 CoreDNS,通常是在 Kubernetes 里。

你先记住几个事实:

  1. 它是集群 DNS
  2. 它是插件化的
  3. 它的配置文件叫 Corefile
  4. 常见插件有 kubernetesforwardcacheloghealth

这些认知当然没错。

但只要你真的开始看源码,很快就会撞上一个非常典型的问题:

CoreDNS 的代码不是那种“一个 server + 一组 handler”就能立刻看明白的结构。

你会马上遇到几层东西交织在一起:

  1. 最外层入口非常薄
  2. 真正启动逻辑在 coremain
  3. 配置解析不是自己从零写的,而是建立在 Caddy 这一套框架之上
  4. 插件并不是手工硬编码装配,而是通过 plugin.cfg 和生成代码来决定编译进来的顺序
  5. 运行时请求处理又是一个典型的链式插件模型

这也是为什么很多人第一次读 CoreDNS,会有一种“每个文件我都看懂了一点,但整体就是串不起来”的感觉。

所以这篇文章不打算先讲某一个插件的细节,而是先做一件更重要的事:

从源码结构角度,把 CoreDNS 的骨架搭出来。

换句话说,读完之后,你至少应该能回答这些问题:

  1. CoreDNS 从 main() 到真正启动 DNS server,中间经过了哪些层
  2. plugin.cfg 到底在整个工程里扮演什么角色
  3. 一个插件是怎么注册进去的
  4. Corefile 是怎么变成一条插件链的
  5. 一次 DNS 请求最后是怎么流过 ServeDNS 链路的
  6. 看 CoreDNS 源码时,应该先看哪些目录,后看哪些目录

本文讨论基于 coredns/coredns 主仓库的源码组织方式。文中会尽量贴着真实文件和真实调用关系来讲,但重点仍然是建立阅读框架,而不是把每一行实现细节都铺开。


1. 先说总览:CoreDNS 的代码结构,本质上是“四层拼装”

如果先不看具体文件名,只从架构上抽象,CoreDNS 大致可以拆成四层:

1. 启动层
   coredns.go -> coremain.Run()

2. 装配层
   plugin.cfg -> 生成代码 -> 注册可用插件与执行顺序

3. 配置层
   Corefile -> Caddy Controller -> dnsserver.Config -> plugin setup

4. 请求执行层
   dnsserver.Server -> plugin.Handler chain -> ServeDNS()

这四层里,最容易让人混乱的地方在于:

  1. 编译时顺序和运行时顺序同时存在
  2. 插件注册和插件执行不是同一件事
  3. Corefile 解析和 DNS 请求处理是两条不同时间线

所以理解 CoreDNS 的第一步,不是先钻进某个插件,而是先把这四层分开。


2. 最薄的入口:coredns.go 几乎什么都没做

CoreDNS 的最外层入口其实非常薄。

你能在仓库根部看到一个典型的 coredns.go,里面最关键的部分大致就是:

_ "github.com/coredns/coredns/core/plugin"
"github.com/coredns/coredns/coremain"

func main() {
    coremain.Run()
}

这段代码的信息量其实很大。

它至少说明了三件事:

  1. 真正的启动逻辑不在 main,而在 coremain.Run()
  2. core/plugin 这个包被匿名导入,不是为了直接调用函数,而是为了触发 init() 副作用
  3. CoreDNS 的很多关键装配动作,发生在程序真正运行之前的包初始化阶段

这是一种非常 Go 的工程风格:

主入口极薄,真正的系统组装分散在包初始化和更深层的启动函数里。

如果你第一次读到这里觉得“为什么 main 这么空”,这是正常反应。因为 CoreDNS 的复杂度并不在入口函数,而在入口函数背后那条装配链路。


3. coremain.Run():真正把程序从“二进制”推进到“服务实例”的地方

既然 main() 很薄,那么第一站自然就该看 coremain

从源码结构上看,coremain 是 CoreDNS 的真正启动层。你能看到类似:

  1. TrapSignals()
  2. flag.Parse()
  3. 启动 Caddy/CoreDNS 实例
  4. 打印版本信息
  5. 进入服务生命周期管理

也就是说,coremain.Run() 的职责不是 DNS 业务本身,而是:

把一个已经完成插件注册的程序,推进到“读取配置、构造 server、启动监听”的运行阶段。

这一步很重要,因为它告诉你:

  1. CoreDNS 自己不是从零写了一整套配置框架
  2. 它借用了 Caddy 的一部分基础设施来承载配置解析和实例生命周期

这也是为什么很多 CoreDNS 源码里会出现 caddy.Controllercaddy.Instance 之类的对象。

如果你不提前接受这一点,后面看到插件 setup(c *caddy.Controller) 时会觉得非常突兀。


4. 真正值得先看的,不只是 main,还有 plugin.cfg

第一次读 CoreDNS 源码时,很多人会先本能地找:

  1. main
  2. server
  3. router

但对 CoreDNS 来说,还有一个同样重要、甚至更值得尽早看的文件:

  1. plugin.cfg

为什么?

因为 CoreDNS 是插件式系统,而 plugin.cfg 本质上决定了:

  1. 哪些插件被编译进二进制
  2. 这些插件的默认执行顺序是什么

这和很多人的第一直觉不同。

很多人会以为“Corefile 里写什么插件,就是什么顺序”。

这只说对了一半。

更准确地说:

  1. plugin.cfg 决定编译期已知插件集合及默认指令顺序
  2. Corefile 决定某个 server block 里到底启用哪些插件、带什么参数

所以如果你不看 plugin.cfg,就很难真正理解:

为什么 CoreDNS 的插件顺序是一件一等公民级别的重要事情。


5. 为什么 plugin.cfg 这么关键:CoreDNS 不是运行时“随便插插件”

CoreDNS 虽然是插件式架构,但它不是那种运行时动态扫描目录、再热插拔任意代码的模型。

它更接近:

编译期决定有哪些插件可以存在,运行期决定其中哪些插件在当前 Corefile 中被启用。

这会带来两个直接结果。

5.1 插件顺序是强约束

比如:

  1. cache 应该包在什么位置
  2. rewrite 应该在 forward 前还是后
  3. errorsloghealth 这种插件应当怎样围绕主解析插件排列

这些不是随便摆的。

5.2 生成代码会把顺序固化到程序里

仓库里你能看到:

  1. plugin.cfg
  2. directives_generate.go
  3. core/plugin/zplugin.go
  4. core/dnsserver/zdirectives.go

再结合 Makefile 里的生成规则,你会发现 CoreDNS 不是手工维护一份巨大注册表,而是通过生成代码把插件列表和指令顺序固化下来。

这也是为什么:

理解 CoreDNS 的插件机制,不能只看 plugin/ 目录,还得看生成链路。


6. 生成代码这一层到底在干什么

从工程角度说,CoreDNS 这里做了一件非常聪明也非常实用的事:

把“插件列表”和“插件顺序”这种容易出错的手工装配信息,尽量交给生成代码处理。

你不需要先钻每个生成脚本的全部细节,但要先抓住产物意义。

大致可以理解成:

  1. plugin.cfg 是输入
  2. 生成脚本读取它
  3. 生成一份用于匿名导入插件包的代码
  4. 再生成一份用于 dnsserver 指令顺序或指令表的代码

所以源码阅读里,这一层回答的是:

  1. 插件怎么被“带进来”
  2. 插件默认链路顺序怎么被固化

你可以把它理解成 CoreDNS 的“编译期插件装配器”。


7. 插件注册是怎么发生的:init() + plugin.Register(...)

一旦接受了“匿名导入插件包只是为了触发副作用”这件事,下一步就很好理解了。

CoreDNS 里的很多插件源码,都会有一个非常标志性的模式:

func init() { plugin.Register("forward", setup) }

或者:

func init() { plugin.Register("whoami", setup) }

这段代码背后的意思很简单:

  1. 插件包一旦被导入
  2. init() 会执行
  3. 插件把自己的名字和 setup 函数注册到全局插件注册表里

也就是说,CoreDNS 的插件注册不是靠某个中央文件硬编码:

  1. forward -> setupForward
  2. log -> setupLog

而是:

每个插件自己在 init() 里声明“我是谁,以及怎么做配置装配”。

这是一种非常典型的 Go 插件式装配手法。


8. setup(c *caddy.Controller):插件配置期真正落点

注册完名字以后,下一个关键对象就是插件自己的 setup

CoreDNS 里非常多插件都会长成这样:

func setup(c *caddy.Controller) error {
    config := dnsserver.GetConfig(c)
    ...
}

或者至少会拿着 c *caddy.Controller 去解析参数。

这说明插件的配置期职责通常有两类:

  1. 从 Corefile 当前上下文里解析自己的配置参数
  2. 把“如何把自己挂进请求链”的逻辑写进当前 server 配置对象里

也就是说,setup 并不是在真正处理 DNS 请求。

它处理的是:

当程序启动并解析 Corefile 时,这个插件应该怎样修改 server 的未来运行形态。

这又是一个很容易混淆的时间线:

  1. setup 发生在启动期
  2. ServeDNS 发生在请求期

把这两条时间线分开,是读 CoreDNS 的关键。


9. dnsserver.GetConfig(c) 这一类调用,说明插件不是孤立配置,而是在修改同一个 server 配置骨架

当你在很多插件里看到:

config := dnsserver.GetConfig(c)

这意味着一件很重要的事:

插件不是各自单独创建 server,而是在共同参与构造同一个 dnsserver 配置对象。

换句话说:

  1. Corefile 的一个 server block
  2. 最终会落成一个 dnsserver.Config
  3. 多个插件在解析阶段都会往这份 config 里挂信息

这些信息可能包括:

  1. 监听地址
  2. zone
  3. TLS/HTTPS/QUIC 相关设置
  4. 插件构造函数链
  5. 一些 server 级特性开关

所以读 CoreDNS 源码时,你应该把 dnsserver.Config 想成:

插件们共同编辑的一份 server 蓝图。


10. 真正的运行时核心:plugin.Handler 接口和 ServeDNS

到这里为止,前面都还是启动和装配。

真正到了请求执行期,CoreDNS 最关键的抽象就出来了:

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

这段接口定义几乎就是整个 CoreDNS 请求执行模型的灵魂。

它说明每个插件在运行期,本质上都要回答两个问题:

  1. 我叫什么
  2. 当收到一个 DNS 请求时,我怎么处理它,或者把它交给下一个插件

你可以把它理解成 HTTP middleware 世界里的 handler chain,只不过处理对象从 HTTP request/response 变成了 DNS *dns.Msgdns.ResponseWriter

一旦抓住这一点,CoreDNS 的大量插件源码就会突然变得可读很多。


11. plugin.NextOrFailure(...):插件链式执行模型最典型的信号

在很多插件的 ServeDNS 里,你会反复看到一种非常标志性的写法:

  1. 如果当前插件命中条件,就自己处理
  2. 如果不命中,调用下一个插件

而这种“调下一个”的味道,常常会通过 plugin.NextOrFailure(...) 这类辅助函数体现出来。

它背后的核心语义其实非常简单:

CoreDNS 运行期不是一个巨大 switch,而是一条按顺序串起来的 handler chain。

例如:

  1. rewrite 可能改 query
  2. cache 可能直接命中并返回
  3. kubernetes 可能解析集群内服务名
  4. forward 可能把请求转发给上游
  5. errorslogdnstap 这种插件则更像围绕主处理链路做包装或观测

所以读插件源码时,最该问的问题不是:

  1. 这个插件会不会处理 DNS

而是:

  1. 它在链上的位置是什么
  2. 它是终结型插件、改写型插件,还是包装型插件

12. 一个典型插件长什么样:以 whoamiforward 这类为例

如果你第一次读 CoreDNS 源码,我其实很建议先看简单插件。

比如:

  1. whoami
  2. errors
  3. log

这类插件通常结构很清楚:

  1. init() 里注册
  2. setup() 里解析极少量参数
  3. ServeDNS() 里做很直接的逻辑

例如 whoami 这种插件,本质上就是:

  1. 拿到请求
  2. 构造响应
  3. 把请求来源、服务端地址等信息吐回去

forward 这种插件则更像典型“主功能插件”:

  1. 判断是否匹配当前 zone
  2. 若匹配则向上游转发
  3. 若不匹配则继续 next

通过这两类插件,你能很快建立对 CoreDNS 插件结构的肌肉记忆。


13. 从请求流角度看一次查询:数据到底怎么走

现在把整条链真正串起来。

一次 DNS 请求的大致路径可以抽象成:

socket packet / DoH / DoQ request
    -> dnsserver.Server
    -> 构造 context
    -> Server.ServeDNS(...)
    -> plugin.Handler chain
    -> 某个插件返回响应或错误码
    -> 写回 dns.ResponseWriter

你还能在 core/dnsserver/server.goserver_tls.goserver_https.goserver_quic.go 这类文件里看到类似结构:

  1. 为请求构造 context
  2. 塞入 server、loop、http request 等上下文信息
  3. s.ServeDNS(...)

这说明 CoreDNS 在不同传输层协议上虽然入口不同,但最终都会尽量汇聚到同一条 ServeDNS 主链路上。

这也是它架构上很漂亮的一点:

入口协议可以多样,但处理模型尽量统一。


14. dnsserver 为什么值得当成一个重点目录看

很多人一开始会把大部分注意力放在 plugin/ 下。

这当然合理,但如果你想真正看清 CoreDNS 的代码结构,core/dnsserver 其实至少同样重要。

因为它承担了下面这些职责:

  1. 管理 server 配置对象
  2. 把插件 setup 阶段积累的信息装配成真正的 server
  3. 承接 UDP/TCP/TLS/HTTPS/QUIC 等不同协议入口
  4. 在真正收到请求时,把请求送入统一 handler 链

换句话说:

plugin/ 决定“每个零件怎么工作”,而 dnsserver/ 决定“这些零件怎样被拼成真正跑起来的 DNS 服务”。

所以读源码时,如果你只盯插件,很容易看到树叶却看不到树干。


15. CoreDNS 的插件,不全是“回答 DNS 的插件”

这是另一个非常值得先建立的直觉。

很多人第一次读插件目录,会自然以为:

  1. 每个插件都在“解析某种 DNS 名称”

其实并不是。

CoreDNS 的插件大致可以粗分成几类:

  1. 真正提供解析结果的主功能插件:如 kubernetesfileetcdforward
  2. 改写类插件:如 rewrite
  3. 缓存与性能类插件:如 cache
  4. 可观测性插件:如 logerrorsdnstap
  5. 安全与协议增强插件:如 tlstsig
  6. 调试或实验型插件:如 whoami

这意味着 CoreDNS 的插件链,更像一条“DNS middleware pipeline”,而不是一堆平行的 resolver。

一旦接受这个视角,你再去看 plugin.cfg 的顺序问题,就会觉得很合理了。


16. 为什么插件顺序在 CoreDNS 里特别重要

因为链式模型天然会让顺序成为语义的一部分。

举个最简单的思路:

  1. 如果 cacheforward 前,它可能直接命中并结束链路
  2. 如果 rewriteforward 后,那它可能根本改不到上游转发前的 query
  3. 如果 log 包在不同位置,它看到的请求/响应视图也可能不同

所以 CoreDNS 的顺序不是“代码排列习惯”,而是:

运行时语义的一部分。

这也是为什么生成 zdirectives.go 这类代码会显得合理:

  1. 顺序是框架级信息
  2. 不适合让每个插件自由随意乱排

17. 读 CoreDNS 源码时,一个高效顺序应该是什么

如果你是第一次系统读 CoreDNS,我更建议按下面这条顺序来,而不是一头扎进最大插件。

第一步:看入口和启动骨架

先看:

  1. coredns.go
  2. coremain/run.go

目标是回答:

  1. 程序怎么启动
  2. Caddy 在哪里介入

第二步:看编译期插件装配

再看:

  1. plugin.cfg
  2. directives_generate.go
  3. 生成产物相关文件

目标是回答:

  1. 插件是怎么被编进来的
  2. 默认顺序怎么决定

第三步:看 plugin/plugin.gocore/dnsserver

目标是回答:

  1. handler 接口是什么
  2. config/server 是怎么组装的
  3. 请求是怎么被送进链路的

第四步:挑简单插件建立感觉

先看:

  1. whoami
  2. errors
  3. log
  4. forward

目标是回答:

  1. 一个插件到底长什么样
  2. setupServeDNS 的职责如何分离

第五步:再看重型插件

最后再去啃:

  1. kubernetes
  2. cache
  3. rewrite
  4. etcd

这样阅读阻力会小很多。


18. 为什么 kubernetes 插件不应该作为第一站

很多人是因为 K8s 才关心 CoreDNS,所以第一反应会是直接看 plugin/kubernetes

这很自然,但通常不是最高效的读法。

因为 kubernetes 插件本身就很重,它里面又叠了:

  1. informer/cache
  2. service、endpoint、pod 名称解析逻辑
  3. zone 匹配和 fallthrough
  4. 反向解析、SRV、A/AAAA 等多种记录处理

如果你还没把:

  1. 插件注册
  2. setup
  3. dnsserver.Config
  4. ServeDNS

这些骨架先看清,就很容易在 kubernetes 这种大插件里迷路。

源码分析里,先看骨架再看重型插件,几乎总是更稳的策略。


19. 从工程设计上看,CoreDNS 为什么会长成这种结构

把源码结构看清以后,你会发现 CoreDNS 这套组织方式其实非常符合它的定位。

它要解决的不是:

  1. 写一个固定功能的 DNS server

而是:

  1. 提供一个可组合的 DNS 处理框架
  2. 允许不同环境按需拼装能力
  3. 让不同协议入口尽量复用同一处理链
  4. 让插件能单独发展、单独解析配置、单独处理请求

所以它自然会演化出:

  1. 极薄入口
  2. 编译期插件装配
  3. 启动期 Corefile 解析
  4. 运行期 handler chain

换句话说,CoreDNS 的代码结构不是“写复杂了”,而是:

它的问题本身就要求它成为一个插件式 DNS 平台,而不是一个单体解析器。


20. 总结:读懂 CoreDNS,关键不是先啃插件细节,而是先把装配链和请求链分开

如果把整篇文章压缩成一句话,那就是:

CoreDNS 的代码结构之所以容易让人一开始读不顺,不是因为某个文件特别难,而是因为它把编译期插件装配、启动期配置解析和运行期请求链三套机制叠在了一起;你只有先把这三条时间线拆开,整个工程骨架才会变清楚。

你真正该抓住的主线是:

  1. coredns.go 很薄,真正入口在 coremain.Run()
  2. plugin.cfg 和生成代码决定了编译期插件集合与顺序
  3. 每个插件用 init() + plugin.Register(name, setup) 注册自己
  4. setup(c *caddy.Controller) 发生在 Corefile 解析期,不是请求处理期
  5. plugin.HandlerServeDNS 定义了运行时插件链
  6. core/dnsserver 是把配置和插件真正拼成 server 的骨架目录

一旦这条主线在脑子里立住,再去读:

  1. forward
  2. cache
  3. rewrite
  4. kubernetes

这些插件时,你就不再是在看一堆零散的 Go 文件,而是在看同一套框架下的不同零件。

如果接着往下写,一个很自然的下一篇就是:

再单独拆 kubernetes 插件源码,看它怎样把 Service、Endpoint、Pod 与 zone/fallthrough 组织成具体的 DNS 应答路径。