☰
Linux BIND DNS 服务器搭建:正向反向解析与主从排错
2026/10/27 13:22:17 网站建设 项目流程

1. 先想清楚为什么自己架一台 DNS:三类真实场景与选型前提

在 Linux 上搭建 DNS 域名解析服务器,这件事的门槛比很多人想象的低,但踩坑率比想象的高得多。原因不是 BIND 难装,而是大多数教程只教你复制粘贴一份named.conf和两个 zone 文件,却不告诉你为什么要这么写、写错了会是什么现象、哪几个字段一旦错了排查起来能耗掉一整天。我自己第一次搭的时候,区域文件里少写了一个结尾的点,结果客户端查www.corp.test返回 NXDOMAIN,日志里干干净净,最后是盯着配置文件逐字符比对才发现的。

先说清楚这篇文章的定位:它面向的是需要在 Linux 上部署一台真正能对外提供服务的 DNS 服务器的人,不管是给内网几百台机器做域名解析、给一台对外提供业务的主机做权威解析,还是做一层本地缓存加速,路径都是相通的。零基础能跟着走下来,有经验的可以重点看第 3、4 章的字段解释和第 7 章的排错链路,那里才有真正的信息量。

1.1 内网域名、解析加速与权威托管,需求完全不同

很多人把"搭 DNS"当成一件事,实际上至少是三种不同的需求,配置方向完全相反。

第一种是内网权威解析。典型场景是公司或者实验室内部有一批服务,机器 IP 是192.168.10.x这种私有段,希望用git.corp.test、db.corp.test这样的名字去访问,而不是记 IP。这种需求下,解析服务器只对自己管理的区域负责,不需要去互联网上查任何东西,递归应该关掉。

第二种是本地缓存加速。内网出口带宽紧张或者上游解析服务偶尔抖动,需要在本机或者网关位置放一台缓存服务器,把常用域名的解析结果暂存下来。这种场景下服务器不管理任何区域,只做转发和缓存,recursion必须打开,但只能对内网开放。

第三种是对外权威托管。自己有域名,需要在国内某台机器上跑一个二级或者三级权威服务器。这种场景对可用性要求最高,通常要主从两台,而且必须正确处理 SOA 的各个时间字段。

三种需求混在一起配,就会出现"既想让它查外网又不想让它当开放解析器"的纠结,最后配出一个既慢又不安全的四不像。我的建议是:一台机器只承担一种角色,如果确实要合并,用第 6 章讲的 view 做逻辑隔离。

1.2 部署位置与机器数量:单机缓存还是主从双机

机器放哪里,直接决定了后面的配置复杂度。内部权威解析器最合适的位置是内网核心交换机旁边的一台固定 IP 机器,所有客户端的resolv.conf或者 DHCP 下发的 DNS 选项都指向它。这里有个容易被忽略的细节:服务器本身的resolv.conf千万不要指向自己,否则它需要向外查询的时候会绕回自己形成环路,表现出来就是所有外部域名解析超时。正确做法是让服务器的上游指向另一个可用的解析地址,或者干脆在named.conf里把转发器写死,不依赖系统resolv.conf。

数量上,如果只是内网几十台机器,一台足够;一旦有对外业务依赖,就必须两台起步做主从。主从的意义不只是宕机切换,更重要的是分担查询压力和给变更留出回滚窗口。我曾经遇到过一台单点服务器因为区域文件写错导致整个内网域名全挂,回滚只能靠手改;如果有从库,主库挂了从库还能继续应答一段时间,那几分钟就是救命的时间。

1.3 软件选型:BIND、dnsmasq、PowerDNS 的取舍

方案适合场景优势局限
BIND权威解析、主从、视图分离功能最全、文档最多、几乎覆盖所有 DNS 特性配置语法严格,一个分号错就起不来
dnsmasq小型内网、配合 DHCP配置极简,几十行搞定,支持 hosts 风格不支持完整的区域传输和复杂视图
PowerDNS大规模、需数据库后端记录存数据库,变更方便,API 友好引入数据库依赖,运维面更大

我自己的习惯是:只要涉及主从、视图、DNSSEC 或者要给别人做权威解析,就选 BIND;如果只是给十几台机器做个名字映射顺带缓存,dnsmasq 更省心。这篇文章以 BIND 为主线,因为它的概念模型最完整,学会了之后换别的工具只是换语法。

还有一点常被忽略:选 BIND 就要接受它的严格性。它的配置文件语法错误是致命的,服务直接起不来;但区域文件的错误往往是"能起来,但解析不对",这种半失败状态才是最麻烦的,所以第 2 章和第 3 章我把两类校验分开讲。

2. BIND 落地的第一层地基:安装、目录结构与主配置骨架

装软件这一步本身没什么技术含量,但装完之后文件在哪、默认配置被改成了什么样,是后面所有问题的源头。不同发行版把 BIND 拆成了不同的包,默认路径、运行用户、chroot 策略、甚至 SELinux 策略都不一样。我见过有人照着某篇教程在 Debian 系机器上找/etc/named.conf,找了半小时以为装失败了,其实文件叫/etc/bind/named.conf。

2.1 发行版差异与被改过的默认路径

RHEL 系和 Debian 系的路径对照,最好在一开始就记清楚,后面所有命令都依赖它:

项目RHEL 系Debian 系
软件包名bind、bind-utilsbind9、bind9-utils
主配置文件/etc/named.conf/etc/bind/named.conf
区域文件目录/var/named//var/cache/bind/
服务名namednamed或bind9
运行用户namedbind

安装命令按发行版走:

# RHEL 系 dnf install -y bind bind-utils # Debian 系 apt update && apt install -y bind9 bind9-utils bind9-dnsutils

装完之后先用systemctl status named(Debian 系是bind9)看一眼,如果直接是 failed,先别急着改配置,多半是默认配置文件里的监听地址或者 SELinux 策略导致的,改之前先确认原始状态,否则你会把"装完就有问题"和"我改出来问题"混在一起,排查难度翻倍。

RHEL 系还有一个历史包袱:老版本的 chroot 会把所有路径重定到/var/named/chroot/下。现在主流的版本已经默认不启用 chroot,但配置文件里的directory指令还是指向/var/named,区域文件的相对路径都基于这个目录。建议所有区域文件路径一律写绝对路径,别用相对路径,省得后面因为directory改动而全部失效。

2.2 named.conf 的分层写法与 include 约定

很多人把整份配置塞进一个named.conf,两百行下来自己都看不清。我推荐从第一天就用分层结构,把不同职责拆到不同文件:

/etc/named.conf # 主入口,只放全局 options 和 include /etc/named.conf.local # 自己管理的区域声明 /etc/named.conf.acl # ACL 定义 /etc/rndc.key # 远程管理密钥

主文件的核心骨架大致是这样:

options { listen-on port 53 { 127.0.0.1; 192.168.10.11; }; listen-on-v6 { none; }; directory "/var/named"; dump-file "/var/named/data/cache_dump.db"; statistics-file "/var/named/data/named_stats.txt"; memstatistics-file "/var/named/data/named_mem_stats.txt"; allow-query { localhost; 192.168.10.0/24; }; recursion yes; allow-recursion { localhost; 192.168.10.0/24; }; dnssec-validation no; # 内网环境先关掉,避免上游不可达时 SERVFAIL forwarders { 192.168.10.1; }; }; include "/etc/rndc.key"; include "/etc/named.conf.local";

几个注释值得展开。listen-on明确列出要监听的地址,比写any安全得多,因为多网卡机器上你可能并不想让某个网卡对外提供服务。dnssec-validation在内网环境我通常先设成no,原因是很多内网的上游解析或者转发链不支持完整的 DNSSEC 校验,一旦开了,某些域名会直接返回 SERVFAIL,而这种失败在dig输出上跟"域名不存在"看起来很像,排查起来极其费劲。等整个链路跑通之后,再考虑第 8 章讲的方式打开。

include的顺序也有讲究:rndc.key要在需要它的语句之前被加载,否则动态更新和远程管理会报找不到密钥。这个错误日志里通常会写couldn't add command channel,看起来跟密钥无关,实际上就是加载顺序问题。

2.3 启动前的两道校验:named-checkconf 与权限归属

配置写完,别急着systemctl restart。BIND 自带两个校验工具,用好了能省下大量时间:

# 检查主配置语法(不检查区域文件内容) named-checkconf -z /etc/named.conf # 单独检查某个区域文件 named-checkzone corp.test /var/named/corp.test.zone

-z参数的作用是顺便把配置里声明的所有区域都加载一遍,能提前发现区域文件不存在、语法错误、SOA 缺失这类问题。我的习惯是每次改完配置必跑一遍,输出为空才算通过。

权限是第二个高频坑。BIND 以named(或bind)用户身份运行,区域文件必须对它可读,而目录必须对它可写(因为要生成.jnl日志文件和.signed签名文件)。标准的设置是:

chown root:named /var/named/corp.test.zone chmod 640 /var/named/corp.test.zone chown named:named /var/named chmod 750 /var/named

这样做的理由:区域文件本身属主是 root,防止被服务进程篡改;组是 named,保证可读;目录属主给 named,保证它能在里面写运行时文件。这套权限组合在 RHEL 系上基本不会出问题,在 Debian 系上把组名换成bind即可。

2.4 放行 53 端口的三种拦截面:防火墙、SELinux、监听地址

服务起不来或者起来了但客户端查不通,90% 集中在三个地方。我把它们列成检查顺序:

  1. 防火墙。RHEL 系用 firewalld,一条命令搞定:
firewall-cmd --permanent --add-service=dns firewall-cmd --reload

Debian 系如果用 ufw,则是ufw allow 53/tcp和ufw allow 53/udp。注意DNS 同时需要 UDP 和 TCP:常规查询走 UDP,超过 512 字节的响应、区域传输、部分 DNSSEC 场景走 TCP。只放 UDP 会出现"小查询正常、大记录失败"的诡异现象。

  1. SELinux。这是最容易被忽略的一层。开启 enforcing 的机器上,named默认只能读特定目录,你自己新建的区域文件目录可能直接被拒绝,日志在/var/log/audit/audit.log里,服务日志里只看到"permission denied"但文件权限明明是好的。三个处理方式:
# 方式一:给文件打上正确的 SELinux 标签 restorecon -Rv /var/named/ # 方式二:允许 named 写主区域文件(动态更新场景需要) setsebool -P named_write_master_zones on # 方式三:临时确认是不是 SELinux 的问题(仅用于诊断) setenforce 0

我一般先setenforce 0试一下,如果立刻好了,就说明是 SELinux,再切回 enforcing 用前两种方式正式解决。诊断阶段可以临时关闭,但绝不能就这么放着,直接把 SELinux 永久关掉是很差的做法,后面再想开回来会发现一堆东西已经依赖它关闭的状态了。

  1. 监听地址。listen-on里没写实际网卡的 IP,服务就只监听127.0.0.1,本机dig一切正常,别的机器死活查不到。用ss -lnup | grep :53一眼就能看出来监听的地址是不是0.0.0.0或者目标网卡 IP。

3. 正向解析区域文件:SOA 的每个字段都在干什么

区域文件是 DNS 服务器的核心,也是最多人"抄了但不知道为什么"的地方。我先给一份可以直接用的完整示例,然后逐个字段拆开讲。假设内网网段是192.168.10.0/24,域名是corp.test,解析服务器本身是192.168.10.11。

3.1 一个最小可用的 corp.test 区域文件什么样

$TTL 3600 $ORIGIN corp.test. @ IN SOA ns1.corp.test. admin.corp.test. ( 2024051201 ; serial 3600 ; refresh 600 ; retry 604800 ; expire 300 ; minimum / negative TTL ) @ IN NS ns1.corp.test. @ IN NS ns2.corp.test. @ IN A 192.168.10.11 ns1 IN A 192.168.10.11 ns2 IN A 192.168.10.12 www IN A 192.168.10.20 git IN A 192.168.10.21 db IN A 192.168.10.22 api IN CNAME www.corp.test. @ IN MX 10 mail.corp.test. mail IN A 192.168.10.25 @ IN TXT "v=spf1 ip4:192.168.10.25 -all"

$TTL是默认生存时间,$ORIGIN是相对名称的基准。这两个指令让后面的记录可以省掉重复的后缀,但它们也是"域名后面多出奇怪后缀"这个经典问题的来源,第 3.4 节专门讲。

3.2 SOA 里那五个数字到底在控制什么

SOA 记录是区域文件的灵魂,缺了它整个区域加载失败,但五个数字的含义经常被含糊带过。逐个说:

serial(序列号):区域的版本号。主从同步的唯一判据就是它——从库发现主库的 serial 比自己大就拉取新数据,相等或更小就什么都不做。常见做法是用YYYYMMDDnn格式,比如2024051201表示 5 月 12 日第 1 次修改。这里有个必须知道的坑:serial 只能单调递增,不是"改了就生效",而是"改大了才生效"。有人手改回一个较小的数字,从库就再也不更新了,而且日志里毫无提示,因为对从库来说"主库版本比我还旧"根本不是错误。

refresh(刷新间隔):从库多久主动问一次主库有没有更新。3600 秒(1 小时)是常见值。调小的代价是查询压力,调大的代价是同步延迟。

retry(重试间隔):从库连不上主库时,隔多久再试。600 秒比较稳妥。这个值设得太小,主库宕机时会看到从库疯了一样重连。

expire(过期时间):从库连续多久联系不上主库,就认为自己的数据彻底失效,停止应答该区域。604800 秒(7 天)是常见配置。它的实际意义是给主库故障留出的最长容忍窗口,设得太短,主库维护几小时,从库就罢工了;设得太长,主库数据早就变了,从库还在用错误数据应答。

minimum(负缓存 TTL):这个字段历史上含义变过,现在的作用是告诉客户端"这个域名不存在的结论可以缓存多久"。300 秒是合理值,太大意味着你新建了一个域名,客户端要等好几分钟才能解析到。

一句话总结这段的实操价值:主从不同步先看 serial,从库罢工先看 expire,改完记录不生效先看 minimum。这三个字段能解释主从相关的绝大多数现象。

3.3 A、CNAME、MX、TXT 的写法与真实踩坑点

A 记录最直白,但要注意一个主机名可以有多条 A 记录,这叫轮询,BIND 默认会按轮转顺序返回,用来做简单的负载分担。不过它没有健康检查,一台挂了客户端还是会被分到,所以真正的负载均衡还是得靠专门的组件。

CNAME 有三条硬规则,违反任意一条都会导致named-checkzone报错或者行为异常:

  • 同一个名字不能既有 CNAME 又有其他类型的记录;
  • CNAME 不能指向另一个 CNAME(虽然不严格禁止,但会导致额外查询和解析变慢);
  • 区域顶点(@)不能设 CNAME,因为顶点必须有 SOA 和 NS 记录。

示例里api指向www.corp.test.,客户端查api.corp.test会拿到 CNAME 再追一次 A,最终得到192.168.10.20。注意目标必须写完整 FQDN 并带结尾点,否则会被加上$ORIGIN变成www.corp.test.corp.test.,这个错误极其常见,而且named-checkzone不会报错,因为它语法上完全合法。

MX 记录前面的数字是优先级,越小越优先。10、20、30 这种间隔是惯例,方便以后在中间插入新记录。MX 指向的目标必须是 A 记录,不能是 CNAME,这是规范要求。

TXT 记录的引号是必须的,内容里有空格时更要注意。SPF 就是靠 TXT 承载的,示例里那行意思是"只有 192.168.10.25 这台机器可以用这个域名发信,其他一律拒绝"。测试环境用-all没问题,但如果哪天真的配了对外发信,忘了把真实出口地址加进去,邮件会全被拒。

3.4 为什么你的域名后面多了一串后缀:结尾那个点

DNS 里的名字分两种:绝对域名(FQDN)和相对域名。绝对域名以点结尾,比如www.corp.test.;相对域名不带结尾的点,会被自动拼上$ORIGIN。

这意味着在$ORIGIN corp.test.的环境下:

你写的实际含义
wwwwww.corp.test.
www.corp.testwww.corp.test.corp.test.
www.corp.test.www.corp.test.

第三条才是对的。所有跨区域的引用,比如 CNAME 目标、NS 目标、MX 目标,都必须带结尾点,否则一定会多出一层后缀。这个错误的表现是:dig能返回结果,但结果是错的,客户端拿到的 IP 可能是个完全不相干的东西,或者干脆 NXDOMAIN。

我处理这类问题的习惯是:写完区域文件先跑named-checkzone,然后dig三个典型记录(顶点 A、一个子域名 A、一个 CNAME),逐个核对返回的全名对不对。别嫌麻烦,这一步能拦住八成的低级错误。

4. 反向解析与网段倒序:PTR 记录不是可选项

很多人搭 DNS 只做正向解析,觉得反向解析可有可无。这在小规模内网确实能用,但只要涉及日志分析、邮件发送、安全审计,反向解析就是刚需——对方的服务器会拿你的 IP 去反查域名来验证身份,查不到就直接判定为可疑。

4.1 in-addr.arpa 的倒序规则与区域命名

反向解析的核心规则只有一条:IP 地址的四个字节要倒过来写。192.168.10.0/24这个网段对应的反向区域名是:

10.168.192.in-addr.arpa

注意是10.168.192,不是192.168.10。区域的声明方式:

zone "10.168.192.in-addr.arpa" IN { type master; file "/var/named/192.168.10.rev"; allow-update { none; }; };

区域文件内容:

$TTL 3600 $ORIGIN 10.168.192.in-addr.arpa. @ IN SOA ns1.corp.test. admin.corp.test. ( 2024051201 3600 600 604800 300 ) @ IN NS ns1.corp.test. 11 IN PTR ns1.corp.test. 20 IN PTR www.corp.test. 21 IN PTR git.corp.test. 22 IN PTR db.corp.test. 25 IN PTR mail.corp.test.

因为$ORIGIN已经限定到/24这个粒度,所以20就代表192.168.10.20。如果要做/16的反向区域,名字就是168.192.in-addr.arpa,记录要写成10.20这种两段形式。

这里有个细节值得留意:反向区域的 NS 和 SOA 里写的是正向域名。这不是笔误,反向区域的权威服务器本身也要有个名字,而那个名字来自正向区域。所以正向区域没有正确配置时,反向区域就算加载成功,验证过程中也可能出问题。

4.2 PTR 与 A 记录必须成对维护的理由

严格来说 DNS 规范并不要求 A 和 PTR 必须成对存在,但运维实践里应该当成硬规则来执行。原因是:只有 A 没有 PTR,任何基于反查的校验都会失败;只有 PTR 没有 A,对方反查得到一个域名,再正查这个域名却查不到 IP,这比查不到更让人怀疑。

我在做安全加固时遇到过这样的情况:某台机器的 A 记录改了,IP 从.20挪到.30,但反向区域忘了改,结果反查.30返回的是另一台机器的名字。这种不一致不会引发任何告警,但会在日志分析里制造混乱,非常难追溯。

所以我的做法是把正向和反向写在同一个变更清单里,每次改 A 记录就同步检查对应的 PTR,改 PTR 就检查 A。听起来很笨,但它能避免一类非常隐蔽的问题。

4.3 批量生成 PTR 的脚本思路

内网机器多的时候手写 PTR 不现实,可以用一段脚本从主机名清单生成:

#!/bin/bash # hosts.txt 每行格式:IP 主机名 while read -r ip name; do last=$(echo "$ip" | awk -F. '{print $4}') printf "%-4s IN PTR %s.corp.test.\n" "$last" "$name" done < hosts.txt

生成之后不要直接覆盖线上文件,先输出到临时文件,用named-checkzone校验通过再替换,并且记得手动把 serial 加一。脚本最容易忘的就是这一条,忘了 serial 从库就永远不同步,而且没有任何报错。

5. 递归、转发与权限边界:别把服务器变成开放解析器

权限配置这一块,是区分"能跑"和"能上生产"的分水岭。我见过太多内网 DNS 直接写allow-query { any; }; recursion yes;,结果被当成公开解析器使用,带宽被打满、日志被刷爆,还带来一堆安全风险。

5.1 recursion 与 allow-recursion 是两个独立开关

这两个参数经常被混为一谈,实际职责不同:

  • recursion yes|no:这台服务器是否具备递归查询能力;
  • allow-recursion { ... }:哪些客户端可以使用递归能力。

如果只写recursion yes不写allow-recursion,BIND 的默认行为是跟随allow-query,也就是谁能查谁就能递归。这在很多教程里被直接忽略,但它是内网 DNS 变成开放解析器的根本原因。

对一台只做内网权威解析的服务器,最保守的配置是:

recursion no; allow-query { localhost; 192.168.10.0/24; };

如果这台机器同时要承担缓存转发职责,就得打开递归,但必须严格限定来源:

recursion yes; allow-recursion { localhost; 192.168.10.0/24; }; allow-query-cache { localhost; 192.168.10.0/24; };

allow-query-cache控制谁能读缓存,跟allow-recursion分开设,可以让内部客户端读缓存但限制谁能触发新的递归查询。这个粒度在流量大的环境里很有用。

5.2 forwarders 与 forward only 的适用场景

转发器解决的是"我这台机器不该自己去根服务器逐级查询"的问题。典型场景是内网只有一台机器能出网,其他机器都靠它。

forwarders { 192.168.10.1; 114.114.114.114; }; forward first;

forward first的意思是先问转发器,转发器不回或者超时才自己去查。forward only则是完全不自己查,转发器挂了就查不了。

选哪个取决于你对内网环境的确定程度:如果内网严格禁止 DNS 直接出网,用forward only,这样配置意图明确,不会被误以为还能回落;如果不确定转发器是否稳定,用forward first,多一层兜底,代价是超时场景下响应会变慢。

还有一点:转发器不要配太多,两三个足够。配五个以上,第一轮全部超时的话,客户端早就放弃了。而且转发器的地址本身不要指向自己,这个环路错误表现出来就是所有外部域名解析超时,日志里能看到大量发往本机 53 端口的查询。

5.3 ACL 与 allow-query、allow-transfer 的收口

ACL 是 BIND 里被严重低估的功能。与其在各种指令里重复写网段,不如先定义:

acl "internal" { 127.0.0.1; 192.168.10.0/24; 192.168.20.0/24; }; acl "slaves" { 192.168.10.12; };

然后在options里统一引用:

allow-query { internal; }; allow-recursion { internal; }; allow-transfer { slaves; };

allow-transfer是最关键的一个。区域传输会把整个区域的记录全部吐出去,如果不限制,任何人都能通过dig axfr corp.test @你的服务器把内网所有主机名和 IP 拿走。这个操作在渗透测试里是标准的踩点步骤。

验证是否收口成功,从一台不在slaves列表里的机器执行:

dig axfr corp.test @192.168.10.11

期望结果是Transfer failed.,如果吐出一大堆记录,说明配置没生效。

5.4 rndc 与 TSIG 密钥:远程管理与动态更新

rndc是 BIND 的管理通道,能远程执行 reload、查看状态、刷新缓存。它依赖一个共享密钥,生成方式:

rndc-confgen -a -c /etc/rndc.key

生成后在named.conf里include进来。这个密钥文件权限必须是640且属主是 root:named,否则等于把管理权限公开了。

动态更新(allow-update)用的是同一套 TSIG 机制,允许特定客户端通过nsupdate增删记录:

zone "corp.test" IN { type master; file "/var/named/corp.test.zone"; allow-update { key "update-key"; }; };

开启动态更新有两个副作用必须提前知道:一是 BIND 会在区域文件旁边生成一个.jnl日志文件,所有动态变更先写日志,需要rndc sync或者定期rndc freeze/thaw才会合并进主文件;二是区域文件所在目录必须对named用户可写,否则所有更新都会失败。我见过有人把目录权限设成755 root:root,结果动态更新一直静默失败,日志里只有一行不太显眼的写入错误。

如果不需要动态更新,就老老实实写allow-update { none; };,这是默认也是推荐做法。

6. 主从复制与视图分离:让解析服务顶得住

主从架构的价值前面提过,这里讲落地。核心配置其实只有主库三行、从库三行,但每行的作用必须清楚。

6.1 主从同步的四行关键配置

主库侧:

zone "corp.test" IN { type master; file "/var/named/corp.test.zone"; allow-transfer { 192.168.10.12; }; also-notify { 192.168.10.12; }; };

从库侧:

zone "corp.test" IN { type slave; masters { 192.168.10.11; }; file "/var/named/slaves/corp.test.zone"; };

also-notify的作用是主库 serial 变化后主动通知从库,而不是等从库按refresh周期来问。这能把同步延迟从一小时缩短到几秒,代价可以忽略,强烈建议加上。如果中间有网络设备拦截,通知可能到不了,从库还是能靠定时刷新兜底,所以这不是单点依赖。

从库的区域文件路径通常放在slaves子目录下,这个目录必须对named可写——从库要自己下载并保存区域文件。这个目录忘了建是新手最常犯的错,表现是服务能启动,但dig查从库返回 REFUSED 或者 SERVFAIL,日志里写着open: file not found之类。

6.2 serial 不自增导致从库"不更新"的排查

这是主从环境第一大问题,而且排查起来很容易跑偏。完整的检查链路我总结成五步:

步骤检查项命令或位置典型现象
1主库 serial 是否已增大查看主库区域文件数字没变
2从库是否收到通知从库日志notify关键字没有相关记录
3从库能否连上主库 53/tcpdig axfr corp.test @主库超时或被拒
4allow-transfer是否包含从库 IP主库配置transfer failed
5从库区域文件是否可写目录权限与 SELinux写入报错

我遇到的最隐蔽一次是第三步:防火墙只放行了 UDP 53,allow-transfer配得完全正确,但从库就是拿不到数据。因为区域传输走 TCP,这个细节在大多数防火墙配置教程里被一笔带过。

6.3 view 做内外网分离的实际写法

如果一台机器要同时给内网和公网提供不同答案,就得用 view。基本思路是先定义客户端分组,再为每个组各写一套 zone:

acl "internal" { 192.168.10.0/24; }; acl "external" { any; }; view "internal-view" { match-clients { internal; }; recursion yes; zone "corp.test" IN { type master; file "/var/named/corp.test.internal.zone"; }; }; view "external-view" { match-clients { external; }; recursion no; zone "corp.test" IN { type master; file "/var/named/corp.test.external.zone"; }; };

内网版本里可以写db.corp.test指向192.168.10.22;公网版本里同一个名字可能指向一个公网地址,或者干脆不写这条记录。这就是所谓的"内外网分离视图"。

view 有几个必须注意的限制:一是所有 zone 必须写在某个 view 内部,不能有游离在外的 zone;二是 view 的匹配顺序是从上往下,第一条命中就停,所以external的any必须放在最后;三是不同 view 里的同名 zone 是完全独立的,serial 各自维护,改了一边别忘另一边。

6.4 动态更新与 DHCP 联动的思路

内网机器地址由 DHCP 分配的场景下,主机名和 IP 的对应关系是动态的,手改区域文件跟不上。标准做法是 DHCP 服务在分配地址后,通过 TSIG 认证向 DNS 发送动态更新,自动写入 A 和 PTR 记录。这个链路配置不复杂,但涉及两个服务的密钥一致性,一旦密钥不匹配,表现就是地址分配正常但反查一直查不到。排查时先用nsupdate手动发一条测试记录,确认 DNS 侧能接受更新,再去查 DHCP 侧的配置,能把问题范围缩小一半。

7. 排错链路:从 dig 到日志的逐层定位方法

DNS 排查最忌讳的就是"东改一下西改一下"。我习惯固定按一条链路走:客户端 → 网络 → 服务监听 → 配置加载 → 区域数据 → 上游。每一层都用一个确定的命令验证,不让猜测进入流程。

7.1 dig 输出逐段解读

dig @192.168.10.11 www.corp.test A

输出里最该看的是这几段:

;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 34567 ;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 1 ;; ANSWER SECTION: www.corp.test. 3600 IN A 192.168.10.20 ;; Query time: 1 msec ;; SERVER: 192.168.10.11#53(192.168.10.11)

status字段的含义直接指向问题类型:

  • NOERROR且有 ANSWER:正常;
  • NOERROR但 ANSWER 为空:域名存在但没有该类型的记录,比如查 AAAA 但只有 A;
  • NXDOMAIN:这个域名在该区域里不存在,先检查区域文件里到底有没有写;
  • SERVFAIL:服务器处理失败,通常是区域文件语法错误、DNSSEC 校验失败、或者转发不可达;
  • REFUSED:被权限拒绝了,几乎一定是allow-query没包含这个来源地址。

flags里的aa表示权威回答,如果查的是自己有 zone 的域名却没有aa,说明请求根本没命中那个 zone,可能 zone 没加载成功,也可能被别的 view 接走了。rd是客户端请求递归,ra是服务器支持递归,从库上看ra为 false 时递归查询会失败,这是正常设计。

7.2 五类高频故障与对应检查点

现象最可能的原因检查命令
客户端完全不通防火墙 / 监听地址ss -lnup | grep :53、firewall-cmd --list-all
本机能查别机不能查listen-on只写了 127.0.0.1查看 named.conf 的 options
返回 REFUSEDallow-query未包含来源检查 ACL 与 allow-query
返回 SERVFAIL区域文件错误 / 上游不可达named-checkzone、看 named 日志
改了记录不生效serial 未增 / 客户端缓存提高 serial、dig +norecurse绕缓存

第三行和第五行是最常见的两个。REFUSED 基本不用怀疑别的原因,就是权限。而"改了不生效"里,如果是从库环境,先排除 serial;如果只有一台机器,那就是 TTL 和客户端缓存的问题,用dig +norecurse直连服务器能绕过缓存立即看到真实结果。

7.3 named-checkzone 与日志级别调整

named-checkzone的完整用法:

named-checkzone -d corp.test /var/named/corp.test.zone

-d会输出详细的加载过程,能看到每一条记录被解析成什么全名。这个功能在排查"域名后缀重复"问题时极其有效,因为它直接把解析后的 FQDN 打出来。

日志方面,BIND 默认的日志级别比较克制,很多问题看不出来。可以在named.conf里加一段:

logging { channel default_log { file "/var/log/named/named.log" versions 5 size 20m; severity info; print-time yes; print-category yes; print-severity yes; }; category default { default_log; }; category xfer-out { default_log; }; category update { default_log; }; };

severity info已经能覆盖大部分排查需求,再往上调到debug会刷出海量日志,只在定位特别刁钻的问题时临时开,定位完立刻关掉,否则日志文件几小时就能撑满磁盘。

8. 长期运行:日志轮转、性能与安全加固

配置跑通只是开始,真正决定这套服务能不能长期用的,是日志、性能和安全这三块。它们平时不显眼,出问题的时候都是大问题。

8.1 query log 的开与关

想知道谁在查什么,就打开查询日志:

logging { channel query_log { file "/var/log/named/query.log" versions 3 size 50m; severity info; print-time yes; }; category queries { query_log; }; };

开启后每一条查询都会记录来源 IP、查询名、查询类型和结果状态。它的价值在于定位异常查询源,比如某台机器在疯狂重试一个不存在的域名,或者某个网段在做区域探测。

但它有两个明显的代价:一是日志写入量巨大,几百台机器的内网每天能产生几百 MB;二是磁盘 IO 有开销。所以我的做法是平时关着,需要排查时临时打开,用rndc reconfig生效,排查完立即关闭。长期开着不仅是资源浪费,写进去的内容也没人会看。

8.2 rate-limit 与 minimal-responses 这两个开关

rate-limit是限制单个客户端查询速率的机制:

rate-limit { responses-per-second 20; window 5; };

它的实际作用是削弱放大攻击的影响——如果你的服务器被利用来做反射放大,响应速率限制能让它不至于把出口带宽全部吃掉。内网环境一般用不上,但只要服务器有公网地址,就应该配上。默认值偏宽松,20 次/秒对正常客户端足够,对攻击流量能形成有效压制。

minimal-responses yes;的作用是不在响应里附带不必要的记录。正常情况下 DNS 响应会带上 AUTHORITY 和 ADDITIONAL 段,里面可能有 NS 记录和它们的 A 记录,这些对客户端解析答案并不必需。开启之后响应体积明显缩小,在高并发场景下能省下可观的带宽。这个参数对排查问题是有一点副作用的:响应变短之后,dig输出里少了那些附加信息,有时候会让人误以为记录缺失。知道这一点就不会被误导。

8.3 DNSSEC 校验要不要开

DNSSEC 校验开启后,服务器会验证上游返回的数据签名,被篡改或者不完整的响应会被拒绝,返回 SERVFAIL。它的安全价值是实实在在的,但落地有两个前提:

一是上游链路必须完整支持。内网的多级转发、某些中间设备会破坏 DNSSEC 相关记录,一旦断了,开启校验后这些域名全部解析失败。

二是排查难度上升。开启后 SERVFAIL 的原因多了一种,而且从现象上跟"上游不可达"很难区分。

我的做法是分阶段:内网刚上线时设dnssec-validation no;把基础链路跑通;稳定运行一段时间后,改为auto观察一周,重点看有没有出现成片的 SERVFAIL;确认无异常再保持。如果确实需要清理某几个域名的校验问题,用negative-trust-anchors做例外处理,而不是把整个校验关掉。

8.4 备份与变更流程

DNS 配置的特点是"改错一次影响全网",所以变更流程比技术本身更重要。我这套流程是踩过坑之后固定下来的:

  1. 改之前先备份:cp corp.test.zone corp.test.zone.$(date +%Y%m%d),配置文件和区域文件一起备份;
  2. 本地校验:named-checkzone和named-checkconf -z都跑一遍,输出必须为空;
  3. 先改从库或者备用机,用dig @备用机验证结果,确认真实返回符合预期;
  4. 再改主库,改完立即用dig从客户端侧验证,不要只看服务器本机;
  5. 保留回滚文件至少一周,确认无问题再清理。

第 3 步是关键。直接改主库然后祈祷不出事,是我早期最常犯的错误。把改动先在备用机上验证,成本只有几十秒,能避免的却是整个内网的解析中断。

监控方面,最朴素的方案往往最有效:一个定时任务,每分钟对几个关键域名做dig,返回非 NOERROR 就告警。这个脚本不到二十行,但能比任何复杂监控更早发现问题——毕竟 DNS 挂了的时候,所有依赖域名的服务都会跟着挂,而它们的报错信息通常完全看不出根因在 DNS 上。

最后分享一个小技巧:把常用的验证命令写成一个脚本,改完配置直接跑一遍,输出所有关键检查项。我自己的脚本里包含主库 SOA 检查、从库 SOA 比对(确认 serial 一致)、正向和反向各三条抽样、一次axfr拒绝测试。这套检查跑完不到五秒,但它拦下的问题比我事后排查的加起来还多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询