前几天帮某公司排查一个内网域名解析故障,应用一直报“Unknown host”,折腾了大半天。后来复盘发现,问题既不在网络,也不在服务进程,而是 DNS 服务器上“监听地址”和“查询权限”的语义没理清。那次之后,我把 Linux DNS 服务从安装到实战配置完整重做了一遍,也记录了非常多容易忽略的细节。
这篇文章就围绕“Linux DNS 服务详解”和“DNS 实战配置”这两个关键词展开,从 Bind 安装、zone 文件编写、dig 验证、故障排查,一直到访问控制、内网转发和视图分流,把一整条链路串起来讲。无论你是刚碰 Linux 的小白,还是配过几次 DNS 但总在排错时卡壳的人,这篇文章都值得存一份作为参考。
1. 一次“域名突然解析不了”的故障现场,让我决定动手重写配置
1.1 表象:应用报 Unknown host,ping IP 却完全正常
某公司有一套部署在内网的系统,某天 web 服务突然连不上数据库,日志里反复出现Unknown host database.internal。第一反应基本都是检查网络:我先 ping 数据库服务器的 IP,通了;再用 curl 访问内网接口的 IP,也正常。
这就很经典了——能通 IP,不能通域名,说明问题十有八九出在域名解析环节。我跑到出问题的机器上看/etc/resolv.conf,nameserver指向的是 192.0.2.10,正是内网自建 DNS 服务器。接着用dig手动查了一下,发现返回状态是REFUSED。服务进程在跑,端口也在监听,但查询被拒绝,说明这台 DNS 根本不是“挂了”,而是“不让我查”。
1.2 我的第一轮误判:以为服务没起来或者端口不通
看到REFUSED之前,我先做了两件事:systemctl status named看进程状态,再用ss -lntp看 53 端口。进程是active (running),端口也监听在 192.0.2.10 和 127.0.0.1 上。端口通、进程活着,为什么查询会被拒?
再翻配置才发现,options里写了allow-query { localhost; };。这个配置表达的意思是:只有本机能向这台 DNS 发起查询,内网其他客户端全部被拒之门外。而问题在于上一任配置只在listen-on里写了0.0.0.0,没在allow-query里放开内网网段。
1.3listen-on管“收包”,allow-query管“回答”,两者不是一回事
很多人容易把listen-on和allow-query混在一起。listen-on决定 named 进程把 UDP/TCP 53 端口绑定到哪个本地 IP,影响的是“网络层让不让数据包到达”。allow-query是应用层的查询白名单,决定“到达了之后,named 愿不愿意处理这个查询”。
可以这么理解:电话线接通到前台,不代表前台愿意给你查资料。就算监听在 0.0.0.0,只要allow-query设置不对,该拒绝一样拒绝。这个故障修起来倒也快,把allow-query改成内网网段就行,但暴露出的问题值得警惕:配置 DNS 时,每一项参数的管理边界必须清楚,不能靠“看着像”去猜。
2. 动手前必须分清楚:权威区、递归查询与缓存定位
2.1 一台 DNS 服务器可能同时在扮演两个角色
配置 Bind 之前,必须想清楚这台服务器在解析链路里的角色。最简单也最容易踩坑的就是把“权威服务器”和“递归解析器”混为一谈。
权威服务器负责回答自己名下 zone 的查询,类似“户籍窗口”。你说www.lab.example是 192.0.2.30,只有我这个窗口说了算。递归解析器则像“问路中介”,客户端把域名丢给它,它替你去根服务器、顶级域服务器、权威服务器层层问,最后把答案拿回来,可能还会缓存。
一台 DNS 可以同时干这两件事。在named.conf里定义了type master的 zone,它就拥有权威区;开启了recursion yes并配置allow-recursion,它就提供递归服务。两个角色混在一起时,故障排查的复杂度会明显上升,所以每写一个参数前先问自己:我现在是在配权威、配递归,还是配缓存?
2.2 最小实验环境如何设计
为了避免污染真实网络,我建议用文档保留地址段来搭实验环境。这样即使配置写错,也不会把网内现有设备搞崩。我这里用一个虚构的模拟项目 X 做底子:域名用lab.example,网段用 192.0.2.0/24,DNS 服务器 IP 是 192.0.2.10,客户端是 192.0.2.20。
为什么选这类地址?192.0.2.0/24、198.51.100.0/24、203.0.113.0/24 是专门留给文档和示例用的网段,不会和真实公网地址冲突。内网实验用它们,既安全又能完整复现各种解析流程。
2.3 zone 文件与资源记录的最小知识
在 zone 文件里,最常见的记录类型有 SOA、NS、A、CNAME、MX。SOA 是整个区的“版本信息”,包含序列号、刷新时间、重试时间、过期时间和负缓存时间;NS 声明哪个服务器是权威;A 记录把域名映射成 IPv4 地址。
举个例子,一条简单的 A 记录就是:
www IN A 192.0.2.30它表示www.lab.example解析到 192.0.2.30。这里的IN是类,表示 Internet 类,绝大多数场景都是它。CNAME 是别名,MX 是邮件交换记录。刚开始配置一个内网解析服务,把 SOA、NS、A 这几种记录写对,基本就能跑起来。
2.4 TTL 与缓存:为什么改完记录不是马上生效
TTL 是资源记录在解析器缓存里的存活时间。比如一条记录的 TTL 是 86400 秒,客户端或递归解析器拿到后会缓存一天。在这个时间内,即使权威服务器上已经改了 IP,客户端拿到的还是旧答案。
所以排错时有个很实用的技巧:直接dig @权威服务器 域名,绕过客户端本地缓存;但递归解析器那一层的缓存不容易绕过,只能等 TTL 过期,或者在测试环境重启服务刷新缓存。理解了这一点,才不会被“明明改了 DNS 却不生效”这种问题困住。
3. Bind 从安装到首个 A 记录生效:一份基本可以直接抄的配置
3.1 安装 Bind 与命令行工具
在绝大多数 Linux 发行版上,安装 Bind 都很简单。用包管理器直接装:
# apt 系列 sudo apt install bind9 bind9-utils # dnf/yum 系列 sudo dnf install bind bind-utils我建议把bind9-utils或bind-utils一并装上,因为后面要用的named-checkconf、named-checkzone、dig、nslookup都在这里面。没有这几个工具,排错效率会直线下降。
装完后,先看看主配置文件位置。常见路径是/etc/bind/named.conf或/etc/named.conf,不同发行版可能会有named.conf.options、named.conf.local这样的 include 文件。为了便于管理,我会单独建一个 zones 目录存放所有 zone 文件,例如/var/lib/bind/zones,避免把一堆记录堆在主目录里。
3.2 named.conf 主配置骨架
我给出的是一份适合内网实验环境的配置。先把全局options写好:
options { directory "/var/lib/bind"; listen-on port 53 { 127.0.0.1; 192.0.2.10; }; listen-on-v6 { none; }; allow-query { any; }; allow-recursion { 192.0.2.0/24; 127.0.0.1; }; allow-transfer { none; }; recursion yes; dnssec-validation auto; }; zone "lab.example" { type master; file "/var/lib/bind/zones/db.lab.example"; };listen-on我只写了本机回环和内网地址,没有写0.0.0.0。如果这台机器有公网网卡,写0.0.0.0等于把 DNS 服务暴露到公网,被扫描后可能被人当作反射放大攻击的跳板。
allow-query { any; }表示允许所有来源查询我这个权威 zone,对于权威区来说通常没问题;但allow-recursion只允许内网网段,限制递归解析的适用范围。allow-transfer { none; }则禁止 zone 传输,避免别人把整个解析记录拖走。
3.3 zone 文件逐行拆解
在/var/lib/bind/zones/db.lab.example里写入以下内容:
$TTL 86400 @ IN SOA ns1.lab.example. admin.lab.example. ( 2024111201 ; serial 7200 ; refresh 3600 ; retry 1209600 ; expire 300 ) ; negative cache TTL @ IN NS ns1.lab.example. @ IN A 192.0.2.10 ns1 IN A 192.0.2.10 www IN A 192.0.2.30 api IN A 192.0.2.31 db IN A 192.0.2.40第一行$TTL 86400给后面所有没有显式写 TTL 的记录一个默认值。SOA 记录里的数字依次代表序列号、刷新时间、重试时间、过期时间、负缓存时间。重点强调两点:
第一,serial 是我手动维护的版本号,每次修改 zone 都必须递增,否则从服务器或下游解析器可能认为“区没有变化”,不重新加载数据。
第二,域名末尾的点不能丢。admin.lab.example.写成admin.lab.example(没有点)会被当成admin.lab.example.lab.example,这是新手最容易犯的错。这里的邮箱不是真邮箱,只是表示管理员联系方式,用点替代@。
3.4 启动服务并用 dig 验证
写完后,先做语法检查:
sudo named-checkconf sudo named-checkzone lab.example /var/lib/bind/zones/db.lab.example如果看到OK,说明配置基本没问题。然后启动服务并设置开机自启:
sudo systemctl enable --now named在客户端 192.0.2.20 上查询:
dig @192.0.2.10 www.lab.example预期ANSWER SECTION里会出现www.lab.example. 86400 IN A 192.0.2.30。这就是第一个 A 记录生效了。再查一个不存在的域名,会看到NXDOMAIN,这也能反证权威区工作正常。
3.5 我最容易踩的坑:目录权限和安全策略
有一段时间我喜欢把 zone 文件放在自己建的新目录里,结果服务一 restart,日志报open: permission denied。问题不一定是你配置写错,而是系统的安全策略限制了 named 进程读取自定义目录。
在部分发行版上,默认策略只允许 named 访问/var/named、/var/lib/bind等特定目录。放在/root或某个家目录里的 zone 文件,即使权限是 644,也可能被拦。最稳妥的做法是:把 zone 文件放到系统认可的 zone 目录再做named-checkzone。如果实在要自定义路径,需要同时处理安全策略,生产环境建议优先走标准路径,省得日后排查麻烦。
4. 解析异常时的取证手段:dig、named-checkzone、日志三件套
4.1 dig 的几种查询方式,每个都用得上
排查 DNS 问题时,dig是使用率最高的工具。它的基本用法是dig @服务器 域名 类型。比如:
dig @192.0.2.10 www.lab.example A如果只想要答案,不加头信息,可以用+short:
dig @192.0.2.10 www.lab.example +short如果要跟踪递归查询的完整路径,用+trace:
dig +trace www.example.com它会从根服务器开始,一级一级展示查询过程。还有一个参数很有用,+norecurse:
dig @192.0.2.10 www.lab.example +norecurse这个命令要求服务器不要做递归解析,只看它自己的权威数据。配合“这台服务器到底有没有这个 zone”的判断,非常高效。
4.2 把 named-checkconf 和 named-checkzone 当格式化工具用
修改 Bind 配置前,我都会跑一遍named-checkconf,它不只会检查语法,还能帮你发现括号忘关、分号缺失这类低级问题。named-checkzone则用来检查 zone 文件,用法是:
sudo named-checkzone lab.example /var/lib/bind/zones/db.lab.example如果返回OK,再 reload。很多线上事故都发生在“改完配置直接 reload,结果语法报错导致全部域名不可用”的场景。正确流程应该是:先named-checkconf,再named-checkzone,然后systemctl reload named,最后看journalctl确认没有新报错。
4.3 NXDOMAIN、SERVFAIL、REFUSED 分别意味着什么
查询 DNS 时返回的 status 字段,是排错的核心线索。下面这张表是我在实际工作中反复对照的:
| 响应码 | 含义 | 常见原因 | 排查方向 |
|---|---|---|---|
| NXDOMAIN | 域名不存在 | 名称写错、zone 里缺这条记录、查的域根本不是这台服务器负责 | 确认域名是否存在,确认查询的域是否在服务器权威区内 |
| SERVFAIL | 服务器处理失败 | zone 文件加载失败、上游解析器不可达、文件权限不足 | 查 named 日志,用+norecurse验证权威区数据 |
| REFUSED | 服务器拒绝响应 | allow-query限制、递归权限不足、不支持的查询类型 | 检查访问控制,确认监听地址与查询来源网段 |
理解这些状态码,能省掉大量“瞎试”的时间。遇到SERVFAIL先怀疑服务端,遇到REFUSED先怀疑权限,遇到NXDOMAIN先确认这个域名是否真的存在。
4.4 一个从 SERVFAIL 开始的完整排查实例
有一次某开发者在模拟项目 X 里新增了一个bbs.lab.example的记录,named-checkzone显示 OK,但 reload 后dig一直返回SERVFAIL。我当时没有直接去翻记录,而是按下面的链路一步步看:
第一步,确认服务状态:systemctl status named,显示 active,排除进程问题。第二步,用dig @192.0.2.10 bbs.lab.example +norecurse,照样SERVFAIL,说明不是递归的问题,而是本机加载 zone 失败。第三步,打开日志:
sudo journalctl -u named --since "5 minutes ago" -f日志里出现了一行关键信息:loading from file failed: permission denied。第四步,检查 zone 文件属性,发现文件被放在了自定义目录下,属主是 root,权限 600,named 进程根本读不了。第五步,把文件移动到标准目录,改成bind:named属主和 640 权限,重新 reload,问题立刻消失。
这个案例里,最花时间的其实是“没想到权限会卡住 zone 加载”。事后总结,遇到SERVFAIL一定要优先去看服务日志,而不是反复改配置。
5. 把服务收进内网:访问控制、视图分流与转发规则
5.1 allow-query 和 allow-transfer 的正确姿势
对内网 DNS 来说,访问控制不能只图省事。如果allow-query写成any,而服务器又开了递归,就容易被外部扫到,变成开放递归解析器。这在真实环境里是要尽量避免的。常规做法是:allow-query只放行内网网段,或者为权威区单独放行;allow-recursion也只放行内网网段。
allow-transfer更敏感,它控制的是 zone 传输权限。默认如果写any,任何客户端都能向服务器请求传输整个 zone 文件,等于把内网所有主机的域名和 IP 分布全盘托出。生产环境建议写成{ none; },或者只允许指定的从服务器 IP。
5.2 view:按来源 IP 返回不同的解析结果
企业中很常见的需求是“内外网解析结果不同”。比如内网访问www.lab.example应该得到 192.0.2.30,外部访问则应该得到 203.0.113.30。Bind 的view就是干这个的。
一个简化配置如下:
view "internal" { match-clients { 192.0.2.0/24; 127.0.0.1; }; recursion yes; zone "lab.example" { type master; file "/var/lib/bind/zones/db.lab.example.internal"; }; }; view "external" { match-clients { any; }; recursion no; zone "lab.example" { type master; file "/var/lib/bind/zones/db.lab.example.external"; }; };match-clients决定哪些来源属于这个 view。内部 view 里的db.lab.example.internal文件把www指向内网地址,外部 view 里的文件指向对外地址。要注意,Bind 中一旦启用 view,所有 zone 都必须定义在 view 内部,否则会报 “out of view” 的错误。
5.3 forwarders:内网环境到底该把解析请求往哪转
很多企业的内网出口并不允许 UDP 53 直接访问外界根服务器。这时如果客户端想解析公网域名,内网 DNS 自身又无法完成递归,就需要配置转发器。
options { forwarders { 10.0.0.1; 10.0.0.2; }; forward only; };这段配置意思是:本地解析不了的域名,统一交给 10.0.0.1 和 10.0.0.2 这两台上游 DNS 处理。forward only表示自己不做完整递归,只转发。
这里有个细节:如果这台服务器同时拥有权威区,本机负责的域名还是会直接从本地 zone 回答,不会拿去转发。配置时要把权威区和转发范围分开理解,避免误伤。
5.4 用 systemd 管理 Bind 服务
配置改完,日常操作离不开 systemd。常用命令是:
sudo systemctl enable --now named sudo systemctl reload named sudo systemctl status named sudo journalctl -u named -freload是平滑重载配置,不会中断正在进行的查询;但如果你改了多个 zone 且只想逐个验证,更稳妥的方式还是先named-checkconf,再reload,最后看日志。不要每次改完配置都restart,生产环境里这会丢失缓存,还会带来短暂的中断窗口。
6. 留给自己的交付前检查清单和两个小习惯
6.1 我可以直接照着跑的检查顺序
每次配完 DNS,我都会按顺序跑一遍以下检查,确认没有遗漏:
sudo named-checkconf sudo named-checkzone lab.example /var/lib/bind/zones/db.lab.example dig @192.0.2.10 www.lab.example +short dig @192.0.2.10 notexist.lab.example dig @192.0.2.10 www.example.com +short sudo journalctl -u named --since "10 minutes ago"第一、第二条检查配置语法;第三条验证本地权威记录;第四条验证 NXDOMAIN 行为;第五条验证递归或转发是否正常;最后一条看有没有隐藏的加载错误或权限报错。这套流程走完,基本可以放心交接。
6.2 两个让我少踩坑的操作习惯
第一个习惯是,每次修改 zone 文件后先递增 serial,再执行named-checkzone。不要小看这一步,很多“改了半天不生效”的怪问题,最终都指向 serial 没变。第二个习惯是,在生产环境不要直接修改原 zone 文件,先备份再改动。这样即使改错,也能迅速回滚到上一个可用版本。
DNS 配置这件事,说难不难,说简单也不简单。大部分故障不是协议太深奥,而是配置语义没理清、验证顺序乱了。把每一层的职责边界搞清楚,再配合一套固定的检查流程,Linux 下的 DNS 服务其实可以非常稳定。