自建 DNS 这件事,站长圈子里的讨论很多,但方案往往被两类工具占据:一类是 dnsmasq,轻量、配置简单,适合家庭和小型内网;一类是 BIND9,功能全、历史悠久,适合做权威解析。但如果你稍微留意过 Kubernetes 生态,会发现里面几乎清一色用的是另一个东西——CoreDNS。它是 CNCF 的毕业项目,用 Go 写,配置方式是链式插件,一份 Corefile 就能表达出传统 DNS 需要几百行 zone 文件才能说清的逻辑。
这篇文章不聊 K8s,而是讲怎么把 CoreDNS 拿来当一台独立的 DNS 服务器用,解决个人站长实际会碰到的问题:内网域名解析、上游分流、缓存加速、DNS 层负载均衡、以及把多个解析需求收口到一台机器上统一管理。读完你会发现,在很多场景里它比 dnsmasq 表达力更强,比 BIND9 轻得多。
一、CoreDNS 是什么,和 dnsmasq、BIND9 差在哪
CoreDNS 的核心设计只有一个概念:插件链(plugin chain)。它启动时按顺序加载一组插件,每个请求依次经过这些插件,谁匹配谁处理。你想要的每一个功能——缓存、转发、日志、健康检查、重写、负载均衡——都是一个插件。这种设计让它极其灵活:想要什么就加一个插件,不想要的插件不加载,零开销。
三者定位对比:
- dnsmasq:一锅端,DHCP + DNS + TFTP 一体,配置是扁平的行式选项,简单但扩展性弱,复杂逻辑(按查询来源分别返回不同结果)很难表达。
- BIND9:权威解析的标杆,zone 文件成熟规范,DNSSEC 支持完善。但配置复杂,做递归缓存和分流要写不少
view和acl。 - CoreDNS:Go 编写,单二进制,无外部依赖。配置是声明式的 Corefile,插件链让它天然适合"转发 + 缓存 + 分流 + 重写"这类场景。
一句话选型:要做权威解析、管 DNS 记录,用 BIND9;要在内网做缓存加速和按域名分流,dnsmasq 和 CoreDNS 都能干,但逻辑越复杂越该选 CoreDNS;要在容器环境里做服务发现,CoreDNS 是唯一选择。
二、安装:单二进制,几乎零依赖
最省事的方式是直接下官方二进制(以 Linux amd64 为例):
cd /tmp
curl -LO https://github.com/coredns/coredns/releases/latest/download/coredns_1.11.1_linux_amd64.tgz
tar -xzf coredns_1.11.1_linux_amd64.tgz
install -m 755 coredns /usr/local/bin/coredns
coredns -version也可以用 Docker 跑,适合不想污染宿主机的场景:
docker run -d --name coredns \
--restart=always \
-p 53:53/udp -p 53:53/tcp \
-v /etc/coredns/Corefile:/Corefile:ro \
coredns/coredns:1.11.1 -conf /Corefile注意:DNS 同时需要 TCP 和 UDP 的 53 端口,两个都要映射;UDP 少了会导致大响应(超过 512 字节且没协商 EDNS0)失败,这是个常见疏漏。
三、认识 Corefile:插件的执行顺序
CoreDNS 的配置叫 Corefile,长这样:
. {
errors
health :8080
cache 300
forward . 8.8.8.8 1.1.1.1
log
}最外层的 . 表示这是一个匹配所有域名的 server block。花括号里按顺序写插件。几个必须有或很常用的:
- errors:把错误打到标准错误,排障必开。
- health :8080:暴露一个 HTTP 健康检查端点
/health,方便监控。 - cache:缓存插件,
cache 300表示缓存 300 秒。这是性能的关键。 - forward:把本地没有的查询转发给上游。
- log:记录每条查询,调试时开,生产环境可关掉省 I/O。
插件的书写顺序会影响行为,因为插件链按这个顺序执行。一般把 errors、health 之类的辅助插件放前面,cache 放在需要缓存的 forward/hosts 之前,log 通常放最后。不确定顺序时,CoreDNS 启动会打印一条 warning 提示你顺序异常,照着改即可。
四、内网自定义域名解析:hosts 插件
dnsmasq 里靠 address=/dev.local/10.0.0.5 做的事,CoreDNS 用 hosts 插件:
. {
hosts {
10.0.0.5 git.dev.local
10.0.0.6 db.dev.local
10.0.0.7 cache.dev.local
fallthrough
}
forward . 8.8.8.8
}也可以用标准 hosts 文件格式,方便和 /etc/hosts 同步:
. {
hosts /etc/coredns/hostsfile {
fallthrough
}
forward . 8.8.8.8
}fallthrough 是必须理解的一个关键字:如果当前插件没有匹配到查询,是否继续往下走。这里 hosts 插件如果没命中 dev.local,就 fallthrough 让后面的 forward 去处理公网域名;如果不写 fallthrough,hosts 插件对未命中会直接返回 NXDOMAIN,导致所有公网域名都解析不了。
五、按域名分流上游:不同域名走不同解析
这是 CoreDNS 比 dnsmasq 强很多的地方。比如:公司内网域名走内网 DNS,国内域名走运营商 DNS,其他走公共 DNS。
# 内部域名走内网 DNS
internal.example.com {
forward . 192.168.1.10
cache 60
}
# 国内域名走运营商
cn {
forward . 202.96.128.86 202.96.128.166
cache 300
}
# 其余走公共 DNS
. {
forward . 8.8.8.8 1.1.1.1
cache 300
log
}每个 server block 匹配一个域名后缀,CoreDNS 会自动把请求路由到最匹配的那个 block。匹配是从后往前按标签逐级找:查 api.internal.example.com 会优先命中 internal.example.com 这个 block,而不是 .。这比 dnsmasq 的 server=/internal.example.com/192.168.1.10 语法更直观,而且每个 block 可以独立配置缓存时间、插件行为。
六、缓存调优:让重复查询不再出网
DNS 缓存的收益非常直接:本地命中就不用再出网查,延迟从几十毫秒降到亚毫秒,还省了上游的 QPS。
cache 300 {
success 9984 300
denial 9984 60
prefetch 10 60s 25%
serve_stale 1h
}逐项说明:
- success:缓存成功响应的最大条目数和 TTL 上限。这里最多缓存 9984 条,最多存 300 秒。
- denial:缓存 NXDOMAIN 这类否定响应。TTL 设短一点(60 秒)比较稳妥,避免域名刚注册好却被旧缓存挡住。
- prefetch:热门条目在 TTL 剩 25% 时提前刷新,避免过期瞬间的"缓存击穿"——大量并发请求同时回源。这对高流量站点很有价值。
- serve_stale:上游全挂时,继续用过期缓存兜底 1 小时。这个特性能显著提升 DNS 层面的可用性。
如果你的 CoreDNS 只服务几十台机器,缓存条目数和 TTL 用默认值也够;但如果面对高并发,prefetch + serve_stale 是必配项。
七、DNS 层负载均衡:多 IP 轮询返回
CoreDNS 有个 loadbalance 插件,能把一个域名对应的多个 A 记录轮询打散,避免客户端总按顺序选第一个 IP(这就是经典的"DNS 轮询"负载均衡):
web.example.com {
hosts {
10.0.0.11 web.example.com
10.0.0.12 web.example.com
10.0.0.13 web.example.com
ttl 30
}
loadbalance round_robin
cache 30
}每次查询返回的 IP 顺序会被打乱,客户端大概率均匀分布到三台机器上。注意这只是最粗粒度的负载均衡,它不感知后端是否存活——机器挂了 DNS 还会把流量分过去。真正需要健康检查的负载均衡应该交给 HAProxy/Nginx,DNS 轮询只适合做简单的流量分摊。
八、验证与排障
启动后先确认监听的端口:
ss -lunp | grep :53
ss -ltnp | grep :53用 dig 直接指定 CoreDNS 查询:
# 查内网自定义域名
dig @127.0.0.1 git.dev.local
# 查公网域名(走 forward)
dig @127.0.0.1 www.baidu.com
# 看缓存是否生效(第二次查询 RTT 应该骤降)
dig @127.0.0.1 example.com | grep "Query time"如果 CoreDNS 不返回结果,按这个顺序排查:
dig @127.0.0.1 ...超时 → 端口没监听或防火墙拦了。检查ufw/firewalld是否放行 53/udp 和 53/tcp。- 本地域名解析失败但公网域名正常 → hosts 插件写错了,或漏了
fallthrough。 - 全部解析失败 → forward 的上游不可达,或 Corefile 语法错误导致服务没起来。看
journalctl -u coredns或容器日志。 - 返回 SERVFAIL → 上游超时,通常是网络出口问题(比如只放行了 UDP 没放 TCP,某些查询需要 TCP)。
九、五个必踩的坑
坑一:只监听 UDP 不监听 TCP。CoreDNS 默认同时监听,但如果防火墙只开了 UDP,带 DNSSEC 或大响应的查询会失败。务必 53/udp 和 53/tcp 都放行。
坑二:hosts 插件漏了 fallthrough。这是新手最常见的错误——配了个内网域名,结果所有公网域名都解析不了。记住:凡是"只处理部分查询"的插件,基本都要加 fallthrough。
坑三:把 CoreDNS 暴露到公网。CoreDNS 默认没有访问控制,一旦 0.0.0.0:53 对公网开放,就成了开放解析器(open resolver),会被利用来做 DNS 放大攻击,轻则被投诉重则被封 IP。正确做法是只监听内网地址:把 .:53 改成 bind 10.0.0.1,或用防火墙限制来源网段。
坑四:缓存 TTL 设得太长。对于经常变更记录的内网环境(比如容器 IP 频繁变动),缓存 300 秒会带来大量"改了不生效"的困惑。这类域名用独立 server block 把 cache 设成 5~30 秒。
坑五:和 systemd-resolved 抢 53 端口。Ubuntu 默认装了 systemd-resolved,它占了 127.0.0.53:53。直接在 53 上起 CoreDNS 会报 address already in use。要么停掉 resolved(systemctl disable --now systemd-resolved),要么让 CoreDNS 监听另一个地址,再把 /etc/resolv.conf 指过去。
十、个人站长的典型用法
最后给一个一站式 Corefile,把上面讲的收口到一份配置里,适合一台内网 DNS 服务器:
. {
errors
health :8080
bind 10.0.0.1
hosts /etc/coredns/hostsfile {
fallthrough
}
cache 300 {
prefetch 10 60s 25%
serve_stale 1h
}
forward . 8.8.8.8 1.1.1.1 {
max_concurrent 1000
policy random
}
loadbalance round_robin
log
}这份配置能同时做:内网域名解析(hosts 文件)、公网解析转发(forward)、缓存加速(cache)、多 IP 轮询(loadbalance),而全部逻辑加起来不到 30 行。对比 BIND9 要实现同样功能所需的 zone 文件加 view 配置,CoreDNS 的优势一目了然。如果你正在用 dnsmasq 但觉得分流逻辑越来越绕,或者受够了 BIND9 的配置复杂度,CoreDNS 值得一试。