1. 为什么我最终选择自建 DNS 服务器
1.1 从一次内网域名解析故障说起
事情的起因很简单。某天早上到办公室,发现内部测试环境的几个服务域名全部解析失败,开发同学没法连数据库,测试同学打不开后台,运维群里瞬间炸锅。排查了一圈,问题出在公共 DNS 服务商那边——他们调整了某个上游节点的解析策略,导致我们内网自定义的那几条 A 记录被覆盖了。这件事让我意识到一个很现实的问题:只要你的域名解析依赖别人,你就永远受制于人。
公共 DNS 服务确实方便,注册个账号、点几下鼠标就能加记录,但它有三个绕不开的硬伤。第一是可控性差,解析策略、TTL 下限、缓存行为全由服务商说了算,你只能被动接受。第二是隐私问题,你内网所有设备的域名查询请求都要经过第三方,哪些机器访问了哪些域名,理论上对方一清二楚。第三是内网场景支持弱,很多公共 DNS 对私有域名、泛解析、条件转发这些需求支持得很别扭,甚至根本不支持。
自建 DNS 服务器就是冲着这三个痛点去的。用 Bind9 在自己的一台内网机器上把解析服务跑起来,所有查询请求都在自己的网络里闭环,想加什么记录就加什么记录,TTL 想设多少设多少,上游转发给谁也是自己说了算。这套方案特别适合几类人:中小团队的内网运维、有多个测试环境需要域名隔离的开发者、家里有 NAS 和一堆自建服务想统一域名管理的技术爱好者,以及准备考网络相关认证、想动手理解 DNS 协议细节的学习者。
Bind9 是这套方案里最经典的选择。它是目前互联网上部署量最大的 DNS 服务软件之一,文档齐全、社区活跃、功能覆盖从最简单的缓存解析到 DNSSEC 签名、动态更新、视图隔离等高级特性。你不需要买任何商业授权,一台 1 核 1G 的机器就能跑得很稳。下面我把整个自建过程拆开讲,包括方案设计、安装配置、实操步骤和踩过的坑,尽量让第一次接触的人也能照着做下来。
2. 整体方案设计与核心思路拆解
2.1 自建 DNS 到底解决哪些问题
在动手之前,得先想清楚这台 DNS 服务器要承担什么角色。很多人一上来就装 Bind9,结果配置写得乱七八糟,最后自己都不知道哪条记录生效了。我的建议是先把需求列清楚,再决定架构。
常见的自建 DNS 需求大概分四类。第一类是纯缓存解析,就是给内网设备提供一个比公共 DNS 更快的本地缓存,第一次查询走上游,后续命中缓存直接返回,适合网络出口带宽紧张或者对解析延迟敏感的场景。第二类是内网私有域名解析,比如给db.internal、git.lab这类只在内部使用的域名做解析,这些域名在公网根本不存在,必须由自己的 DNS 来回答。第三类是条件转发,把特定域名(比如公司主域)的查询转发到指定的上游 DNS,其他查询走另一条线路,实现分流。第四类是访问控制与日志审计,记录内网设备都查询了哪些域名,便于排查问题和做安全分析。
这四类需求可以叠加。我自己的部署就是“缓存 + 私有域名 + 条件转发”三合一:内网设备把 DNS 指向这台机器,私有域名由本地 zone 文件回答,公司主域转发到指定上游,其余查询走公共上游并缓存。这样一套下来,既解决了内网域名解析的自主可控,又保留了访问公网的能力。
2.2 为什么是 Bind9 而不是其他方案
市面上能自建 DNS 的软件不少,dnsmasq、CoreDNS、Unbound、PowerDNS 各有各的定位。我选 Bind9 主要基于三点考虑。
第一是功能完整度。Bind9 支持完整的 zone 文件语法、视图(view)隔离、DNSSEC、动态更新(TSIG 认证)、响应策略区(RPZ)等,这些在 dnsmasq 上要么没有,要么实现得很简陋。如果你以后想按来源 IP 返回不同解析结果(比如内网返回私有地址、外网返回公网地址),Bind9 的 view 功能是现成的,dnsmasq 就做不到。
第二是稳定性和生态。Bind9 在互联网上跑了二十多年,各种边界情况都被踩过,配置文件语法虽然啰嗦但极其明确。遇到问题搜一下,几乎都能找到答案。相比之下 CoreDNS 更偏云原生场景,配置是插件链式的,理解成本对传统运维来说反而更高。
第三是学习价值。如果你想真正搞懂 DNS 协议,Bind9 的 zone 文件、资源记录类型、SOA 字段、区域传送这些概念是最贴近 RFC 标准的。学完 Bind9,再去看其他 DNS 软件就是降维打击。
当然 Bind9 也有缺点,配置相对繁琐,一个字符写错就可能启动失败,日志也不够友好。但这些恰恰是逼你理解协议细节的动力。下面进入实操。
2.3 部署架构与网络规划
在正式安装前,先把网络规划定下来。我假设的场景是这样的:一台内网服务器,IP 是192.168.10.53,操作系统用常见的 Linux 发行版,内网网段是192.168.10.0/24。这台机器同时作为内网 DNS 和上游转发器。
关键决策点有三个。第一,监听地址。Bind9 默认只监听127.0.0.1,必须改成监听内网网卡地址或者any,否则内网其他机器查询不到。第二,允许查询的客户端范围。一定要用 ACL 限制,只允许内网网段查询,否则你的 DNS 就成了开放解析器,会被滥用。第三,上游转发器选谁。建议选两个以上稳定的公共 DNS 作为转发目标,避免单点故障。
注意:开放递归解析是非常危险的操作。一旦你的 DNS 对公网开放递归查询,很容易被利用来做放大攻击。务必用
allow-query和allow-recursion严格限制来源。
架构上我推荐“转发 + 本地权威”的混合模式。对于公网域名,Bind9 自己不递归,而是转发给上游;对于内网私有域名,Bind9 作为权威服务器直接回答。这样既减轻了本机负担,又保证了内网域名的自主可控。
3. 核心细节解析与实操要点
3.1 安装 Bind9 与目录结构说明
安装本身很简单,不同发行版的包管理器命令略有差异。以常见的两类系统为例:
# Debian/Ubuntu 系 sudo apt update sudo apt install bind9 bind9utils bind9-doc # RHEL/CentOS 系 sudo yum install bind bind-utils装完之后,先别急着改配置,花两分钟把目录结构搞清楚,后面能省很多事。Bind9 的核心文件分布如下:
| 路径 | 作用 |
|---|---|
/etc/bind/named.conf | 主配置文件,通常只做 include |
/etc/bind/named.conf.options | 全局选项,监听、转发、ACL 都在这 |
/etc/bind/named.conf.local | 本地自定义 zone 的声明 |
/etc/bind/named.conf.default-zones | 默认的根、localhost 等 zone |
/var/cache/bind/ | 缓存和动态更新的 zone 文件存放处 |
/var/log/syslog或/var/named/data/ | 日志输出位置 |
Debian 系习惯把配置拆成多个文件用 include 串起来,RHEL 系则倾向于集中在一个named.conf里。理解这个差异很重要,因为网上搜到的教程可能来自不同发行版,直接照抄会找不到文件。
3.2 全局选项配置的关键参数
打开named.conf.options,这是整个配置里最需要动脑子的地方。我给出一个经过实际验证的配置模板,然后逐条解释为什么这么写。
acl "internal" { 127.0.0.1; 192.168.10.0/24; }; options { directory "/var/cache/bind"; listen-on port 53 { 127.0.0.1; 192.168.10.53; }; listen-on-v6 { none; }; allow-query { internal; }; allow-recursion { internal; }; allow-transfer { none; }; forwarders { 223.5.5.5; 119.29.29.29; }; forward only; recursion yes; dnssec-validation auto; max-cache-size 256M; max-cache-ttl 86400; min-cache-ttl 60; version "not disclosed"; };逐条说。acl定义了一个名为 internal 的地址集合,把本机和内网网段放进去,后面所有权限控制都引用它,改一处即可全局生效。listen-on明确指定监听回环和内网地址,不监听公网,从源头上杜绝外部访问。allow-query和allow-recursion都限制为 internal,这是防止开放解析的核心。
forwarders里填两个上游 DNS,forward only表示自己不递归、只转发。这里有个细节:如果你不加forward only,Bind9 在转发失败时会尝试自己从根服务器递归,这在纯内网环境里往往超时,反而拖慢解析。dnssec-validation auto开启 DNSSEC 验证,能识别被篡改的解析结果,建议保持开启。
max-cache-size限制缓存占用内存,防止小内存机器被撑爆。max-cache-ttl和min-cache-ttl控制缓存时间上下限,min-cache-ttl设成 60 秒可以避免上游返回极短 TTL 导致频繁回源。最后version把版本号藏起来,减少信息泄露。
提示:
forwarders里的地址请根据自己实际网络环境选择就近的、稳定的解析服务,不要照抄。不同地区、不同运营商的最优上游可能不同,建议用dig实测延迟后再定。
3.3 私有 zone 文件的编写要点
全局选项搞定后,接下来是私有域名的解析。假设我们要管理一个内部域名lab.internal,需要在named.conf.local里声明:
zone "lab.internal" { type master; file "/etc/bind/db.lab.internal"; allow-update { none; }; };type master表示这是主权威服务器,file指向 zone 文件路径,allow-update none禁止动态更新(除非你有 TSIG 密钥,否则别开)。然后创建 zone 文件,内容大致如下:
$TTL 3600 @ IN SOA ns1.lab.internal. admin.lab.internal. ( 2024010101 ; serial 3600 ; refresh 1800 ; retry 604800 ; expire 86400 ; minimum ) @ IN NS ns1.lab.internal. ns1 IN A 192.168.10.53 db IN A 192.168.10.20 git IN A 192.168.10.21 nas IN A 192.168.10.30 *.test IN A 192.168.10.40这里有几个容易踩坑的点。serial 号每次修改 zone 文件都必须递增,否则从服务器不会同步,虽然单机场景影响不大,但养成习惯很重要,我一般用日期加序号。SOA 里的邮箱写法特殊,admin.lab.internal.实际代表admin@lab.internal,那个点不能漏。泛解析用*.test可以匹配a.test、b.test等所有子域名,做测试环境隔离时特别有用。
改完 zone 文件后,务必用named-checkzone校验语法:
named-checkzone lab.internal /etc/bind/db.lab.internal输出zone lab.internal/IN: loaded serial 2024010101才算通过。这个命令能提前发现绝大多数语法错误,比启动失败后再翻日志高效得多。
3.4 条件转发与视图隔离的进阶用法
如果你有多个上游,想按域名分流,条件转发就派上用场了。比如公司主域corp.example的解析必须走内部 DNS,其他走公共上游:
zone "corp.example" { type forward; forwarders { 192.168.10.1; }; forward only; };这样对corp.example的查询会专门转发到192.168.10.1,其余域名仍走全局 forwarders。这个功能在多环境、多租户场景里非常实用。
再进一步是视图隔离。假设同一台 DNS 既要服务内网又要服务一个隔离的测试网段,希望它们对同一域名得到不同结果,可以用 view:
view "internal" { match-clients { 192.168.10.0/24; }; recursion yes; zone "lab.internal" { type master; file "/etc/bind/db.lab.internal"; }; }; view "test" { match-clients { 192.168.20.0/24; }; recursion yes; zone "lab.internal" { type master; file "/etc/bind/db.lab.internal.test"; }; };注意 view 一旦启用,所有 zone 都必须定义在 view 内部,包括根 zone 和 localhost,否则会报错。这是新手最容易卡住的地方。视图的匹配是按顺序的,第一个匹配上的 view 生效,所以要把范围小的放前面。
4. 实操过程与核心环节实现
4.1 从零到可用的完整操作流程
把前面的碎片串起来,走一遍完整流程。假设你拿到一台干净的 Linux 机器,IP 已配好为192.168.10.53。
第一步,安装软件包,命令前面给过了。第二步,编辑named.conf.options,填入 3.2 节的配置模板,注意把 ACL 里的网段改成你自己的。第三步,编辑named.conf.local,声明私有 zone。第四步,创建 zone 文件并校验。第五步,检查主配置语法:
named-checkconf这个命令不输出任何内容就代表配置没问题。第六步,启动服务并设置开机自启:
sudo systemctl enable --now named # 部分系统服务名是 bind9 sudo systemctl enable --now bind9第七步,验证监听状态:
sudo ss -tulnp | grep :53应该能看到 named 进程监听在127.0.0.1:53和192.168.10.53:53。如果只看到回环地址,说明listen-on没生效,回去检查配置。
4.2 用 dig 验证解析是否正常
服务起来后,用dig做几组验证。先测私有域名:
dig @192.168.10.53 db.lab.internal +short期望输出192.168.10.20。再测公网域名走转发:
dig @192.168.10.53 www.example.com +short能返回 IP 就说明转发链路通了。再测缓存是否生效,连续查两次同一个域名,第二次的 Query time 应该明显更短:
dig @192.168.10.53 www.example.com | grep "Query time"第一次可能是几十毫秒,第二次通常降到 0 到 1 毫秒,这就是本地缓存在起作用。最后测一下递归限制,从一台不在 ACL 里的机器查询,应该被拒绝:
dig @192.168.10.53 www.example.com # 期望看到 REFUSED 或超时这几组测试覆盖了权威解析、转发、缓存、访问控制四个核心功能,全部通过才算部署成功。
4.3 客户端如何切换到自建 DNS
服务端搞定后,客户端要指向这台 DNS。Linux 上改/etc/resolv.conf:
nameserver 192.168.10.53但要注意,很多系统上resolv.conf会被网络管理工具覆盖,正确做法是在网络配置里改。用 systemd-resolved 的系统可以改/etc/systemd/resolved.conf里的DNS=字段,然后重启服务。Windows 客户端在网卡属性里手动填 DNS 地址即可。
如果内网设备多,最省事的办法是在 DHCP 服务器上把下发的 DNS 地址改成这台机器,这样所有设备自动生效,不用一台台改。这也是我推荐的做法,集中管理,后期换 DNS 只改一处。
注意:切换 DNS 后,客户端本地可能有旧缓存。Linux 上可以
systemd-resolve --flush-caches,Windows 上ipconfig /flushdns,否则可能看到旧结果,误以为配置没生效。
4.4 日志配置与查询审计
默认情况下 Bind9 的查询日志是不记录的,想看谁查了什么,需要单独开 query log。在named.conf.options里加:
logging { channel query_log { file "/var/log/named/query.log" versions 5 size 20m; severity info; print-time yes; print-category yes; }; category queries { query_log; }; };记得先创建目录并给 named 用户写权限:
sudo mkdir -p /var/log/named sudo chown bind:bind /var/log/named开启后,每次查询都会记录客户端 IP、查询域名、查询类型。这个日志在排查“某台机器为什么解析不了”时特别有用,也能帮你发现内网有没有设备在异常查询。不过要注意,查询日志增长很快,一定要配versions和size做轮转,否则磁盘很快被写满。生产环境如果查询量大,建议只在排查问题时临时开启。
5. 常见问题与排查技巧实录
5.1 启动失败与配置语法问题
Bind9 启动失败最常见的原因就是配置语法错误。养成习惯,每次改完配置先跑named-checkconf,改完 zone 文件先跑named-checkzone,能拦下九成问题。如果服务还是起不来,看日志:
sudo journalctl -u named -n 50 --no-pager # 或 sudo tail -f /var/log/syslog | grep named几个高频错误:zone 文件里漏了结尾的点,导致域名被拼成db.lab.internal.lab.internal;SOA 记录的括号没配对;serial 号没递增导致从服务器不同步;view 启用后根 zone 没放进 view 里。这些错误日志里通常有明确提示,仔细读一般能定位。
5.2 解析超时与转发链路排查
如果私有域名能解析但公网域名超时,问题多半在转发链路上。排查顺序是这样:先用dig @上游IP www.example.com直接测上游通不通,如果不通说明是网络出口或上游本身的问题;如果上游通但经过 Bind9 不通,检查forwarders配置和forward only是否写对;再看防火墙有没有放行本机到上游的 53 端口出站。
还有一种情况是解析很慢但不超时,通常是上游响应慢或者 DNSSEC 验证卡住。可以临时把dnssec-validation改成no测试,如果变快就是 DNSSEC 链路问题,检查系统时间是否准确——DNSSEC 对时间非常敏感,时间偏差超过签名有效期就会验证失败。
5.3 缓存污染与 TTL 异常
有时候改了 zone 文件里的记录,客户端却还是拿到旧 IP,这通常是缓存导致的。先确认 serial 号递增了,然后重启服务或重新加载:
sudo rndc reload如果还不行,检查客户端的本地缓存和中间网络设备(比如某些路由器)的 DNS 缓存。TTL 设置也影响缓存时长,测试环境建议把 TTL 设短一点,比如 60 秒,方便快速生效;生产环境可以设长一些减少查询压力。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 服务启动失败 | 配置语法错误 | named-checkconf、看日志 |
| 内网查不到私有域名 | zone 未声明或文件路径错 | 检查named.conf.local和文件权限 |
| 公网域名解析超时 | 转发器不可达 | 直接 dig 上游、查防火墙出站 |
| 解析结果被拒绝 | 客户端不在 ACL 内 | 检查allow-query和网段 |
| 改了记录不生效 | 缓存未过期或 serial 未增 | rndc reload、清客户端缓存 |
| 解析偶发变慢 | DNSSEC 验证或上游抖动 | 检查系统时间、换上游测试 |
| 日志磁盘被写满 | query log 未轮转 | 配置 versions 和 size |
5.5 我踩过的几个坑
第一个坑是忘了关 IPv6 监听。有些系统默认启用了 IPv6,Bind9 会尝试在 IPv6 地址上监听,如果网络环境不支持,启动时会报错或卡住。加一行listen-on-v6 { none; };就能解决。
第二个坑是zone 文件权限。zone 文件如果属主不对或者权限过宽,Bind9 会拒绝加载。建议设成root:bind且权限640,既安全又能正常读取。
第三个坑是rndc 密钥不匹配。用rndc reload时报密钥错误,通常是rndc.key和named.conf里的密钥不一致。重新生成一次rndc-confgen -a即可。
第四个坑是上游 DNS 返回的结果被本地缓存后,上游改了记录但本地还在用旧的。这个没法完全避免,只能通过合理设置max-cache-ttl来控制最长缓存时间,重要域名可以单独设短 TTL。
6. 性能调优与长期维护建议
6.1 小内存机器的参数调优
如果你的 DNS 跑在 1G 内存的小机器上,默认配置可能偏保守或偏激进,需要微调。核心参数是max-cache-size,建议设成物理内存的 25% 到 50%,比如 1G 内存设 256M 比较稳妥。recursive-clients控制并发递归客户端数,默认 1000 对小机器偏大,可以降到 200 到 500。tcp-clients同理,按实际并发量调整。
另外可以开启minimal-responses yes,让 Bind9 在响应时只返回必要记录,减少响应体积。对于纯转发场景,additional-section-format之类的参数影响不大,不用折腾。
6.2 监控与告警该看哪些指标
DNS 是基础设施,挂了整个内网都受影响,所以监控很有必要。重点看几个指标:服务进程是否存活,用 systemd 的自动重启加上外部探活;查询响应时间,可以用dig定时探测并记录耗时;缓存命中率,通过统计 query log 里递归查询和缓存命中的比例来估算;磁盘空间,尤其是开了 query log 之后。
简单的做法是写个定时脚本,每分钟dig一次关键域名,失败就告警。复杂一点可以上专业的监控系统采集 Bind9 的统计接口数据。对中小团队来说,前者已经够用。
6.3 备份与迁移的注意事项
zone 文件和配置文件一定要纳入版本管理或定期备份。我习惯把/etc/bind/整个目录用 git 管起来,每次改动都提交,这样出问题能快速回滚,也能看到历史变更。备份时注意 zone 文件里的 serial 号,迁移到新机器后如果 serial 比旧机器小,从服务器可能拒绝同步。
迁移到新机器时,除了拷贝配置和 zone 文件,还要注意新机器的 IP 是否变了。如果变了,listen-on、ACL、zone 文件里的 NS 记录和 A 记录都要同步更新,否则会出现解析指向旧地址的问题。这一步很容易漏,建议迁移前列个清单逐项核对。
6.4 安全加固的几个实用动作
除了前面说的限制查询来源,还有几个加固动作值得做。关闭版本泄露,前面配置里的version "not disclosed";就是干这个的。限制区域传送,allow-transfer { none; };防止别人把你的 zone 文件整个拉走。启用响应策略区(RPZ),如果你有已知的恶意域名列表,可以用 RPZ 直接在 DNS 层拦截,比在每台机器上装杀毒软件高效得多。
定期更新软件,Bind9 历史上出过几个安全漏洞,保持版本更新是最基本的防护。最小权限运行,确保 named 进程以非 root 用户运行,Debian 系默认就是 bind 用户,RHEL 系默认是 named 用户,不要图省事改成 root。
6.5 后续可以扩展的方向
这套基础架构搭好之后,往上还能叠不少东西。比如接入DNSSEC 签名,让自己的私有域名也支持验证,防止内网中间人篡改解析。比如用TSIG 密钥开启动态更新,配合自动化脚本实现“服务上线自动注册域名”。再比如把 Bind9 的查询日志接入日志分析平台,做内网访问行为分析,发现异常外联。
如果内网规模继续扩大,单台 Bind9 可能扛不住,可以搭主从架构,用 zone 传送做冗余,前端再加一层负载均衡。这些扩展都建立在今天这套基础配置之上,把基础打牢,后面加功能就是水到渠成的事。
我个人在实际维护中的体会是,自建 DNS 最大的价值不在于省了多少钱,而在于把解析这件事的控制权拿回了自己手里。出问题时你能看到完整的日志,能随时改记录,能按自己的需求定制解析策略,这种确定性是公共 DNS 给不了的。刚开始配置确实会踩坑,但踩过一遍之后,你对整个网络解析链路的理解会上一个台阶,后面再遇到任何 DNS 相关问题,排查起来都会心里有底。