经典项目源码分析|CoreDNS 代码结构总览:入口、插件链、请求流与 dnsserver 骨架
很多人第一次接触 CoreDNS,通常是在 Kubernetes 里。
你先记住几个事实:
- 它是集群 DNS
- 它是插件化的
- 它的配置文件叫
Corefile - 常见插件有
kubernetes、forward、cache、log、health
这些认知当然没错。
但只要你真的开始看源码,很快就会撞上一个非常典型的问题:
CoreDNS 的代码不是那种“一个 server + 一组 handler”就能立刻看明白的结构。
你会马上遇到几层东西交织在一起:
- 最外层入口非常薄
- 真正启动逻辑在
coremain - 配置解析不是自己从零写的,而是建立在 Caddy 这一套框架之上
- 插件并不是手工硬编码装配,而是通过
plugin.cfg和生成代码来决定编译进来的顺序 - 运行时请求处理又是一个典型的链式插件模型
这也是为什么很多人第一次读 CoreDNS,会有一种“每个文件我都看懂了一点,但整体就是串不起来”的感觉。
所以这篇文章不打算先讲某一个插件的细节,而是先做一件更重要的事:
从源码结构角度,把 CoreDNS 的骨架搭出来。
换句话说,读完之后,你至少应该能回答这些问题:
- CoreDNS 从
main()到真正启动 DNS server,中间经过了哪些层 plugin.cfg到底在整个工程里扮演什么角色- 一个插件是怎么注册进去的
Corefile是怎么变成一条插件链的- 一次 DNS 请求最后是怎么流过
ServeDNS链路的 - 看 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()
这四层里,最容易让人混乱的地方在于:
- 编译时顺序和运行时顺序同时存在
- 插件注册和插件执行不是同一件事
- Corefile 解析和 DNS 请求处理是两条不同时间线
所以理解 CoreDNS 的第一步,不是先钻进某个插件,而是先把这四层分开。
2. 最薄的入口:coredns.go 几乎什么都没做
CoreDNS 的最外层入口其实非常薄。
你能在仓库根部看到一个典型的 coredns.go,里面最关键的部分大致就是:
_ "github.com/coredns/coredns/core/plugin"
"github.com/coredns/coredns/coremain"
func main() {
coremain.Run()
}
这段代码的信息量其实很大。
它至少说明了三件事:
- 真正的启动逻辑不在
main,而在coremain.Run() core/plugin这个包被匿名导入,不是为了直接调用函数,而是为了触发init()副作用- CoreDNS 的很多关键装配动作,发生在程序真正运行之前的包初始化阶段
这是一种非常 Go 的工程风格:
主入口极薄,真正的系统组装分散在包初始化和更深层的启动函数里。
如果你第一次读到这里觉得“为什么 main 这么空”,这是正常反应。因为 CoreDNS 的复杂度并不在入口函数,而在入口函数背后那条装配链路。
3. coremain.Run():真正把程序从“二进制”推进到“服务实例”的地方
既然 main() 很薄,那么第一站自然就该看 coremain。
从源码结构上看,coremain 是 CoreDNS 的真正启动层。你能看到类似:
TrapSignals()flag.Parse()- 启动 Caddy/CoreDNS 实例
- 打印版本信息
- 进入服务生命周期管理
也就是说,coremain.Run() 的职责不是 DNS 业务本身,而是:
把一个已经完成插件注册的程序,推进到“读取配置、构造 server、启动监听”的运行阶段。
这一步很重要,因为它告诉你:
- CoreDNS 自己不是从零写了一整套配置框架
- 它借用了 Caddy 的一部分基础设施来承载配置解析和实例生命周期
这也是为什么很多 CoreDNS 源码里会出现 caddy.Controller、caddy.Instance 之类的对象。
如果你不提前接受这一点,后面看到插件 setup(c *caddy.Controller) 时会觉得非常突兀。
4. 真正值得先看的,不只是 main,还有 plugin.cfg
第一次读 CoreDNS 源码时,很多人会先本能地找:
mainserverrouter
但对 CoreDNS 来说,还有一个同样重要、甚至更值得尽早看的文件:
plugin.cfg
为什么?
因为 CoreDNS 是插件式系统,而 plugin.cfg 本质上决定了:
- 哪些插件被编译进二进制
- 这些插件的默认执行顺序是什么
这和很多人的第一直觉不同。
很多人会以为“Corefile 里写什么插件,就是什么顺序”。
这只说对了一半。
更准确地说:
plugin.cfg决定编译期已知插件集合及默认指令顺序Corefile决定某个 server block 里到底启用哪些插件、带什么参数
所以如果你不看 plugin.cfg,就很难真正理解:
为什么 CoreDNS 的插件顺序是一件一等公民级别的重要事情。
5. 为什么 plugin.cfg 这么关键:CoreDNS 不是运行时“随便插插件”
CoreDNS 虽然是插件式架构,但它不是那种运行时动态扫描目录、再热插拔任意代码的模型。
它更接近:
编译期决定有哪些插件可以存在,运行期决定其中哪些插件在当前 Corefile 中被启用。
这会带来两个直接结果。
5.1 插件顺序是强约束
比如:
cache应该包在什么位置rewrite应该在forward前还是后errors、log、health这种插件应当怎样围绕主解析插件排列
这些不是随便摆的。
5.2 生成代码会把顺序固化到程序里
仓库里你能看到:
plugin.cfgdirectives_generate.gocore/plugin/zplugin.gocore/dnsserver/zdirectives.go
再结合 Makefile 里的生成规则,你会发现 CoreDNS 不是手工维护一份巨大注册表,而是通过生成代码把插件列表和指令顺序固化下来。
这也是为什么:
理解 CoreDNS 的插件机制,不能只看 plugin/ 目录,还得看生成链路。
6. 生成代码这一层到底在干什么
从工程角度说,CoreDNS 这里做了一件非常聪明也非常实用的事:
把“插件列表”和“插件顺序”这种容易出错的手工装配信息,尽量交给生成代码处理。
你不需要先钻每个生成脚本的全部细节,但要先抓住产物意义。
大致可以理解成:
plugin.cfg是输入- 生成脚本读取它
- 生成一份用于匿名导入插件包的代码
- 再生成一份用于 dnsserver 指令顺序或指令表的代码
所以源码阅读里,这一层回答的是:
- 插件怎么被“带进来”
- 插件默认链路顺序怎么被固化
你可以把它理解成 CoreDNS 的“编译期插件装配器”。
7. 插件注册是怎么发生的:init() + plugin.Register(...)
一旦接受了“匿名导入插件包只是为了触发副作用”这件事,下一步就很好理解了。
CoreDNS 里的很多插件源码,都会有一个非常标志性的模式:
func init() { plugin.Register("forward", setup) }
或者:
func init() { plugin.Register("whoami", setup) }
这段代码背后的意思很简单:
- 插件包一旦被导入
init()会执行- 插件把自己的名字和
setup函数注册到全局插件注册表里
也就是说,CoreDNS 的插件注册不是靠某个中央文件硬编码:
forward -> setupForwardlog -> setupLog
而是:
每个插件自己在 init() 里声明“我是谁,以及怎么做配置装配”。
这是一种非常典型的 Go 插件式装配手法。
8. setup(c *caddy.Controller):插件配置期真正落点
注册完名字以后,下一个关键对象就是插件自己的 setup。
CoreDNS 里非常多插件都会长成这样:
func setup(c *caddy.Controller) error {
config := dnsserver.GetConfig(c)
...
}
或者至少会拿着 c *caddy.Controller 去解析参数。
这说明插件的配置期职责通常有两类:
- 从 Corefile 当前上下文里解析自己的配置参数
- 把“如何把自己挂进请求链”的逻辑写进当前 server 配置对象里
也就是说,setup 并不是在真正处理 DNS 请求。
它处理的是:
当程序启动并解析 Corefile 时,这个插件应该怎样修改 server 的未来运行形态。
这又是一个很容易混淆的时间线:
setup发生在启动期ServeDNS发生在请求期
把这两条时间线分开,是读 CoreDNS 的关键。
9. dnsserver.GetConfig(c) 这一类调用,说明插件不是孤立配置,而是在修改同一个 server 配置骨架
当你在很多插件里看到:
config := dnsserver.GetConfig(c)
这意味着一件很重要的事:
插件不是各自单独创建 server,而是在共同参与构造同一个 dnsserver 配置对象。
换句话说:
- Corefile 的一个 server block
- 最终会落成一个
dnsserver.Config - 多个插件在解析阶段都会往这份 config 里挂信息
这些信息可能包括:
- 监听地址
- zone
- TLS/HTTPS/QUIC 相关设置
- 插件构造函数链
- 一些 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 请求执行模型的灵魂。
它说明每个插件在运行期,本质上都要回答两个问题:
- 我叫什么
- 当收到一个 DNS 请求时,我怎么处理它,或者把它交给下一个插件
你可以把它理解成 HTTP middleware 世界里的 handler chain,只不过处理对象从 HTTP request/response 变成了 DNS *dns.Msg 和 dns.ResponseWriter。
一旦抓住这一点,CoreDNS 的大量插件源码就会突然变得可读很多。
11. plugin.NextOrFailure(...):插件链式执行模型最典型的信号
在很多插件的 ServeDNS 里,你会反复看到一种非常标志性的写法:
- 如果当前插件命中条件,就自己处理
- 如果不命中,调用下一个插件
而这种“调下一个”的味道,常常会通过 plugin.NextOrFailure(...) 这类辅助函数体现出来。
它背后的核心语义其实非常简单:
CoreDNS 运行期不是一个巨大 switch,而是一条按顺序串起来的 handler chain。
例如:
rewrite可能改 querycache可能直接命中并返回kubernetes可能解析集群内服务名forward可能把请求转发给上游errors、log、dnstap这种插件则更像围绕主处理链路做包装或观测
所以读插件源码时,最该问的问题不是:
- 这个插件会不会处理 DNS
而是:
- 它在链上的位置是什么
- 它是终结型插件、改写型插件,还是包装型插件
12. 一个典型插件长什么样:以 whoami、forward 这类为例
如果你第一次读 CoreDNS 源码,我其实很建议先看简单插件。
比如:
whoamierrorslog
这类插件通常结构很清楚:
init()里注册setup()里解析极少量参数ServeDNS()里做很直接的逻辑
例如 whoami 这种插件,本质上就是:
- 拿到请求
- 构造响应
- 把请求来源、服务端地址等信息吐回去
而 forward 这种插件则更像典型“主功能插件”:
- 判断是否匹配当前 zone
- 若匹配则向上游转发
- 若不匹配则继续 next
通过这两类插件,你能很快建立对 CoreDNS 插件结构的肌肉记忆。
13. 从请求流角度看一次查询:数据到底怎么走
现在把整条链真正串起来。
一次 DNS 请求的大致路径可以抽象成:
socket packet / DoH / DoQ request
-> dnsserver.Server
-> 构造 context
-> Server.ServeDNS(...)
-> plugin.Handler chain
-> 某个插件返回响应或错误码
-> 写回 dns.ResponseWriter
你还能在 core/dnsserver/server.go、server_tls.go、server_https.go、server_quic.go 这类文件里看到类似结构:
- 为请求构造 context
- 塞入 server、loop、http request 等上下文信息
- 调
s.ServeDNS(...)
这说明 CoreDNS 在不同传输层协议上虽然入口不同,但最终都会尽量汇聚到同一条 ServeDNS 主链路上。
这也是它架构上很漂亮的一点:
入口协议可以多样,但处理模型尽量统一。
14. dnsserver 为什么值得当成一个重点目录看
很多人一开始会把大部分注意力放在 plugin/ 下。
这当然合理,但如果你想真正看清 CoreDNS 的代码结构,core/dnsserver 其实至少同样重要。
因为它承担了下面这些职责:
- 管理 server 配置对象
- 把插件 setup 阶段积累的信息装配成真正的 server
- 承接 UDP/TCP/TLS/HTTPS/QUIC 等不同协议入口
- 在真正收到请求时,把请求送入统一 handler 链
换句话说:
plugin/ 决定“每个零件怎么工作”,而 dnsserver/ 决定“这些零件怎样被拼成真正跑起来的 DNS 服务”。
所以读源码时,如果你只盯插件,很容易看到树叶却看不到树干。
15. CoreDNS 的插件,不全是“回答 DNS 的插件”
这是另一个非常值得先建立的直觉。
很多人第一次读插件目录,会自然以为:
- 每个插件都在“解析某种 DNS 名称”
其实并不是。
CoreDNS 的插件大致可以粗分成几类:
- 真正提供解析结果的主功能插件:如
kubernetes、file、etcd、forward - 改写类插件:如
rewrite - 缓存与性能类插件:如
cache - 可观测性插件:如
log、errors、dnstap - 安全与协议增强插件:如
tls、tsig - 调试或实验型插件:如
whoami
这意味着 CoreDNS 的插件链,更像一条“DNS middleware pipeline”,而不是一堆平行的 resolver。
一旦接受这个视角,你再去看 plugin.cfg 的顺序问题,就会觉得很合理了。
16. 为什么插件顺序在 CoreDNS 里特别重要
因为链式模型天然会让顺序成为语义的一部分。
举个最简单的思路:
- 如果
cache在forward前,它可能直接命中并结束链路 - 如果
rewrite在forward后,那它可能根本改不到上游转发前的 query - 如果
log包在不同位置,它看到的请求/响应视图也可能不同
所以 CoreDNS 的顺序不是“代码排列习惯”,而是:
运行时语义的一部分。
这也是为什么生成 zdirectives.go 这类代码会显得合理:
- 顺序是框架级信息
- 不适合让每个插件自由随意乱排
17. 读 CoreDNS 源码时,一个高效顺序应该是什么
如果你是第一次系统读 CoreDNS,我更建议按下面这条顺序来,而不是一头扎进最大插件。
第一步:看入口和启动骨架
先看:
coredns.gocoremain/run.go
目标是回答:
- 程序怎么启动
- Caddy 在哪里介入
第二步:看编译期插件装配
再看:
plugin.cfgdirectives_generate.go- 生成产物相关文件
目标是回答:
- 插件是怎么被编进来的
- 默认顺序怎么决定
第三步:看 plugin/plugin.go 和 core/dnsserver
目标是回答:
- handler 接口是什么
- config/server 是怎么组装的
- 请求是怎么被送进链路的
第四步:挑简单插件建立感觉
先看:
whoamierrorslogforward
目标是回答:
- 一个插件到底长什么样
setup和ServeDNS的职责如何分离
第五步:再看重型插件
最后再去啃:
kubernetescacherewriteetcd
这样阅读阻力会小很多。
18. 为什么 kubernetes 插件不应该作为第一站
很多人是因为 K8s 才关心 CoreDNS,所以第一反应会是直接看 plugin/kubernetes。
这很自然,但通常不是最高效的读法。
因为 kubernetes 插件本身就很重,它里面又叠了:
- informer/cache
- service、endpoint、pod 名称解析逻辑
- zone 匹配和 fallthrough
- 反向解析、SRV、A/AAAA 等多种记录处理
如果你还没把:
- 插件注册
setupdnsserver.ConfigServeDNS链
这些骨架先看清,就很容易在 kubernetes 这种大插件里迷路。
源码分析里,先看骨架再看重型插件,几乎总是更稳的策略。
19. 从工程设计上看,CoreDNS 为什么会长成这种结构
把源码结构看清以后,你会发现 CoreDNS 这套组织方式其实非常符合它的定位。
它要解决的不是:
- 写一个固定功能的 DNS server
而是:
- 提供一个可组合的 DNS 处理框架
- 允许不同环境按需拼装能力
- 让不同协议入口尽量复用同一处理链
- 让插件能单独发展、单独解析配置、单独处理请求
所以它自然会演化出:
- 极薄入口
- 编译期插件装配
- 启动期 Corefile 解析
- 运行期 handler chain
换句话说,CoreDNS 的代码结构不是“写复杂了”,而是:
它的问题本身就要求它成为一个插件式 DNS 平台,而不是一个单体解析器。
20. 总结:读懂 CoreDNS,关键不是先啃插件细节,而是先把装配链和请求链分开
如果把整篇文章压缩成一句话,那就是:
CoreDNS 的代码结构之所以容易让人一开始读不顺,不是因为某个文件特别难,而是因为它把编译期插件装配、启动期配置解析和运行期请求链三套机制叠在了一起;你只有先把这三条时间线拆开,整个工程骨架才会变清楚。
你真正该抓住的主线是:
coredns.go很薄,真正入口在coremain.Run()plugin.cfg和生成代码决定了编译期插件集合与顺序- 每个插件用
init() + plugin.Register(name, setup)注册自己 setup(c *caddy.Controller)发生在 Corefile 解析期,不是请求处理期plugin.Handler和ServeDNS定义了运行时插件链core/dnsserver是把配置和插件真正拼成 server 的骨架目录
一旦这条主线在脑子里立住,再去读:
forwardcacherewritekubernetes
这些插件时,你就不再是在看一堆零散的 Go 文件,而是在看同一套框架下的不同零件。
如果接着往下写,一个很自然的下一篇就是:
再单独拆 kubernetes 插件源码,看它怎样把 Service、Endpoint、Pod 与 zone/fallthrough 组织成具体的 DNS 应答路径。