CoreDNS 自建 DNS 服务器实战:插件链配置、内网域名解析、按域名分流上游与缓存调优

自建 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 值得一试。

Last modification:October 6th, 2026 at 09:24 pm

Leave a Comment